MCP通信流程与Java版Server端实现

518 阅读6分钟

连接生命周期

MCP为客户端-服务端连接定义了严格的生命周期,以确保适当的能力协商和状态管理。通信流程包括:

  • 初始化:能力协商和协议版本协议。
  • 操作:正常协议通信。
  • 关闭:连接的优雅终止。

初始化

初始化阶段必须是客户端和服务器之间的第一次交互,在初始化完成前,服务端不会处理任何其他消息。在此阶段,客户端和服务器:确认协议版本兼容性,交换和协商能力,共享实现细节。

客户端必须通过发送包含以下内容的初始化请求来启动此阶段:支持的协议版本,客户端能力,客户端实现信息。JSON-RPC消息示例如下:

{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": {
        "protocolVersion": "2024-11-05",
        "capabilities": {
            "roots": {
                "listChanged": true
            },
            "sampling": {

            }
        },
        "clientInfo": {
            "name": "ExampleClient",
            "version": "1.0.0"
        }
    }
}

服务器必须用自己的能力和信息进行响应,JSON-RPC消息示例如下:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2024-11-05",
    "capabilities": {
      "logging": {},
      "prompts": {
        "listChanged": true
      },
      "resources": {
        "subscribe": true,
        "listChanged": true
      },
      "tools": {
        "listChanged": true
      }
    },
    "serverInfo": {
      "name": "ExampleServer",
      "version": "1.0.0"
    },
    "instructions": "Optional instructions for the client"
  }
}

初始化成功后,客户端必须发送一个初始化的通知,表明它已经准备好开始正常的操作,JSON-RPC消息格式如下:

{
  "jsonrpc": "2.0",
  "method": "notifications/initialized"
}

操作

在操作阶段,客户端和服务器根据协商的功能交换消息。双方应:

  • 尊重协商的协议版本。
  • 只使用成功协商的能力。

Tool

  • 获取工具列表。
# 客户端请求
{"jsonrpc":"2.0","method":"tools/list","id":2}

# 服务端响应
 {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"getAllBooks","description":"获取所有的书","inputSchema":{"type":"object","properties":{}}},{"name":"getBookByYear","description":"根据年份获取对应年份的书","inputSchema":{"type":"object","properties":{"year":{"type":"number"}},"required":["year"]}}]}}
  • 工具调用
# 客户端请求
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"getAllBooks","arguments":{}},"id":3}

# 服务端响应
{"jsonrpc":"2.0","id":3,"result":{"content":\[{"type":"text","text":"Book\[title=Java怎么学, url=<https://www.baidu.com>, year=2025]"},{"type":"text","text":"Book\[title=程序员怎么养生, url=<https://www.baidu.com>, year=2024]"},{"type":"text","text":"Book\[title=ai的发展, url=<https://www.baidu.com>, year=2001]"},{"type":"text","text":"Book\[title=时间简史, url=<https://www.baidu.com>, year=2008]"}],"isError":false}

Resource

  • 获取资源列表
# 客户端请求
{"jsonrpc":"2.0","method":"resources/list","id":2}

# 服务端响应
{"jsonrpc":"2.0","id":2,"result":{"resources":\[{"uri":"custom://resource","name":"示例资源","description":"这是一个示例资源","mimeType":"text/plain"}]}}
  • 读取资源
# 客户端请求
{"jsonrpc":"2.0","method":"resources/read","params":{"uri":"custom://resource"},"id":3}

# 服务端响应
{"jsonrpc":"2.0","id":3,"result":{"contents":\[{"uri":"custom://resource/content","mimeType":"text/plain","text":"这是资源内容示例"}]}}

Prompt

  • 获取提示词列表
# 客户端请求
{"jsonrpc":"2.0","method":"prompts/list","id":2}

# 服务端响应
{"jsonrpc":"2.0","id":2,"result":{"prompts":\[{"name":"greeting","description":"生成问候语","arguments":\[{"name":"name","description":"用户名称","required":true}]}]}}
  • 获取提示词(很诡异,SDK的提示词中居然没有System的)
# 客户端请求
{"jsonrpc":"2.0","method":"prompts/get","params":{"name":"greeting","arguments":{"name":"Tom"}},"id":3}

# 服务端响应
{"jsonrpc":"2.0","id":3,"result":{"description":"为用户Tom生成的问候语","messages":\[{"role":"assistant","content":{"type":"text","text":"你是一个友好的助手,请为用户生成问候语"}},{"role":"user","content":{"type":"text","text":"你好,请给我一个友好的问候"}},{"role":"assistant","content":{"type":"text","text":"你好,Tom!很高兴见到你。今天过得怎么样?"}}]}}

关闭

在关闭阶段,一方(通常是客户端)干净地终止协议连接,并没有定义特定的关闭消息,应该使用底层传输机制来表示连接终止。

服务端

POM配置

<dependency>
  <groupId>io.modelcontextprotocol.sdk</groupId>
  <artifactId>mcp</artifactId>
  <version>0.9.0</version>
</dependency>

<!-- Used by the HttpServletSseServerTransport -->
<dependency>
  <groupId>jakarta.servlet</groupId>
  <artifactId>jakarta.servlet-api</artifactId>
  <version>6.1.0</version>
</dependency>
<dependency>
  <groupId>org.apache.tomcat.embed</groupId>
  <artifactId>tomcat-embed-core</artifactId>
  <version>11.0.2</version>
</dependency>
<dependency>
  <groupId>org.apache.tomcat.embed</groupId>
  <artifactId>tomcat-embed-websocket</artifactId>
  <version>11.0.2</version>
</dependency>

代码实现

MCP资源

  • Tool
public class BookToolSpec {

  public static McpServerFeatures.SyncToolSpecification getAllBookSpec() {
   var schema = """
     {
       "type" : "object",
       "properties" : {
       }
     }
     """;
   return new McpServerFeatures.SyncToolSpecification(new McpSchema.Tool("getAllBooks", "获取所有的书", schema),
     (exchange, arguments) -> {
      // 工具的实现
      List<Book> bookList = new BookTool().getBooks();
      List<McpSchema.Content> result = bookList.stream()
       .map(p -> new McpSchema.TextContent(p.toString()))
       .collect(Collectors.toUnmodifiableList());
      return new McpSchema.CallToolResult(result, false);
     });
  }

  public static McpServerFeatures.SyncToolSpecification getBookByYearSpec() {
   var schema = """
     {
       "type" : "object",
       "properties" : {
         "year" : {
           "type" : "number"
         }
       },
       "required" : ["year"]
     }
     """;
   return new McpServerFeatures.SyncToolSpecification(new McpSchema.Tool("getBookByYear", "根据年份获取对应年份的书", schema),
     (exchange, arguments) -> {
      // 工具的实现
      Integer year = (Integer) arguments.get("year");
      List<Book> bookList = new BookTool().getBookListByYear(year);
      List<McpSchema.Content> result = bookList.stream()
       .map(p -> new McpSchema.TextContent(p.toString()))
       .collect(Collectors.toUnmodifiableList());
      return new McpSchema.CallToolResult(result, false);
     });
  }

  public static class BookTool {

   private final List<Book> books = new ArrayList<>();

   public BookTool() {
    var one = new Book("Java怎么学", "https://www.baidu.com", 2025);
    var two = new Book("程序员怎么养生", "https://www.baidu.com", 2024);
    var three = new Book("ai的发展", "https://www.baidu.com", 2001);
    var four = new Book("时间简史", "https://www.baidu.com", 2008);
    this.books.addAll(List.of(one, two, three, four));
   }

   public List<Book> getBooks() {
    return books;
   }

   public List<Book> getBookListByYear(int year) {
    return books.stream().filter(p -> p.year() == year).collect(Collectors.toList());
   }

  }

  public record Book(String title, String url, int year) {

  }

}
  • Resource
public class ExampleResourceSpec {

  public static McpServerFeatures.SyncResourceSpecification getExampleResourceSpec() {
   return new McpServerFeatures.SyncResourceSpecification(
     new McpSchema.Resource("custom://resource", "示例资源", "这是一个示例资源", "text/plain", null),
     (exchange, request) -> {
      List<McpSchema.ResourceContents> contents = new ArrayList<>();
      String content = "这是资源内容示例";
      contents
       .add(new McpSchema.TextResourceContents("custom://resource/content", "text/plain", content));
      return new McpSchema.ReadResourceResult(contents);
     });
  }

}
  • Prompt
public class GreetingPromptSpec {

  public static McpServerFeatures.SyncPromptSpecification getGreetingPromptSpec() {
   return new McpServerFeatures.SyncPromptSpecification(
     new McpSchema.Prompt("greeting", "生成问候语", List.of(new McpSchema.PromptArgument("name", "用户名称", true))),
     (exchange, request) -> {
      List<McpSchema.PromptMessage> messages = new ArrayList<>();
      String name;
      name = new String(((String) request.arguments().get("name")).getBytes(StandardCharsets.UTF_8),
        StandardCharsets.UTF_8);
      if (name.isEmpty()) {
       name = "访客";
      }
      // 创建对话消息序列
      McpSchema.PromptMessage userMessage = new McpSchema.PromptMessage(McpSchema.Role.USER,
        new McpSchema.TextContent("你好,请给我一个友好的问候"));
      McpSchema.PromptMessage systemMessage = new McpSchema.PromptMessage(McpSchema.Role.ASSISTANT,
        new McpSchema.TextContent("你是一个友好的助手,请为用户生成问候语"));
      McpSchema.PromptMessage assistantMessage = new McpSchema.PromptMessage(McpSchema.Role.ASSISTANT,
        new McpSchema.TextContent("你好," + name + "!很高兴见到你。今天过得怎么样?"));
      messages.add(systemMessage);
      messages.add(userMessage);
      messages.add(assistantMessage);
      return new McpSchema.GetPromptResult("为用户" + name + "生成的问候语", messages);
     });
  }

}

MCP通信

  • MCPTransportType
public enum MCPTransportType {

  stdio,

  sse

}
  • MCPTransportFactory
public class MCPTransportFactory {

  public static McpServerTransportProvider createTransportProvider(MCPTransportType type) {
    return switch (type) {
      case stdio -> createStdioTransportProvider();
      case sse -> createSseTransportProvider();
      default -> throw new IllegalArgumentException("Invalid transport type: " + type);
    };
  }

  private static McpServerTransportProvider createStdioTransportProvider() {
    return new StdioServerTransportProvider(new ObjectMapper());
  }

  private static McpServerTransportProvider createSseTransportProvider() {
    Tomcat tomcat = new Tomcat();
    tomcat.setPort(8080);
    String baseDir = System.getProperty("java.io.tmpdir");
    tomcat.setBaseDir(baseDir);
    Context context = tomcat.addContext("", baseDir);
    HttpServletSseServerTransportProvider
        transportProvider = new HttpServletSseServerTransportProvider(new ObjectMapper(), "/message");
    Tomcat.addServlet(context, "transportProvider", transportProvider);
    context.addServletMappingDecoded("/sse", "transportProvider");
    context.addServletMappingDecoded("/message", "transportProvider");
    try {
      tomcat.start();
      tomcat.getConnector();
    } catch (LifecycleException e) {
      throw new RuntimeException("Failed to start Tomcat", e);
    }
    return transportProvider;
  }

}

MCP Server

public class MCPServer {

  public static void main(String[] args) {
   McpServerTransportProvider transportProvider = MCPTransportFactory.createTransportProvider(MCPTransportType.sse);

   // 配置mcp-server信息
   McpSyncServer server = McpServer.sync(transportProvider)
    .serverInfo("custom-mcp-server", "1.0.0") // 设置服务器标识
    .capabilities(McpSchema.ServerCapabilities.builder()
     .tools(true) // 启用工具功能
     .resources(true, true) // 启用资源读写功能
     .prompts(true) // 启用提示功能
     .logging() // 启用日志功能
     .build())
    .build();

   // 添加工具
   server.addTool(BookToolSpec.getAllBookSpec());
   server.addTool(BookToolSpec.getBookByYearSpec());
   // 添加资源
   server.addResource(ExampleResourceSpec.getExampleResourceSpec());
   // 添加提示词
   server.addPrompt(GreetingPromptSpec.getGreetingPromptSpec());

   // 发送日志通知
   server.loggingNotification(McpSchema.LoggingMessageNotification.builder()
    .level(McpSchema.LoggingLevel.INFO)
    .logger("custom-logger")
    .data("Custom MCP server initialized")
    .build());

   // 关闭服务器
   // syncServer.close();
  }

}

测试验证

stdio

  • 服务启动控制台输出
{"jsonrpc":"2.0","method":"notifications/tools/list_changed"}
{"jsonrpc":"2.0","method":"notifications/tools/list_changed"}
{"jsonrpc":"2.0","method":"notifications/resources/list_changed"}
{"jsonrpc":"2.0","method":"notifications/prompts/list_changed"}
{"jsonrpc":"2.0","method":"notifications/message","params":{"level":"info","logger":"custom-logger","data":"Custom MCP server initialized"}}
  • 连接初始化

image.png

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{"roots":{"listChanged":true},"sampling":{}},"clientInfo":{"name":"ExampleClient","version":"1.0.0"}}}
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"logging":{},"prompts":{"listChanged":true},"resources":{"subscribe":true,"listChanged":true},"tools":{"listChanged":true}},"serverInfo":{"name":"custom-mcp-server","version":"1.0.0"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
  • Tool使用 image.png

    • 获取工具列表
{"jsonrpc":"2.0","method":"tools/list","id":2}
{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"getAllBooks","description":"获取所有的书","inputSchema":{"type":"object","properties":{}}},{"name":"getBookByYear","description":"根据年份获取对应年份的书","inputSchema":{"type":"object","properties":{"year":{"type":"number"}},"required":["year"]}}]}}
    • 工具调用
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"getAllBooks","arguments":{}},"id":3} {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"Book[title=Java怎么学, url=https://www.baidu.com, year=2025]"},{"type":"text","text":"Book[title=程序员怎么养生, url=https://www.baidu.com, year=2024]"},{"type":"text","text":"Book[title=ai的发展, url=https://www.baidu.com, year=2001]"},{"type":"text","text":"Book[title=时间简史, url=https://www.baidu.com, year=2008]"}],"isError":false}}

sse

  • 服务启动控制台输出

image.png

  • 连接初始化

image.png

    • 建立连接
$ curl 'http://127.0.0.1:8080/sse' event: endpoint data: /message?sessionId=b5c9ba22-fb64-45ca-acb7-8a3c40f86509
    • 客户端与服务端协商
$ curl -X POST 'http://127.0.0.1:8080/message?sessionId=b5c9ba22-fb64-45ca-acb7-8a3c40f86509' -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{"roots":{"listChanged":true},"sampling":{}},"clientInfo":{"name":"ExampleClient","version":"1.0.0"}}}'
    • 客户端确认
$ curl -X POST 'http://127.0.0.1:8080/message?sessionId=b5c9ba22-fb64-45ca-acb7-8a3c40f86509' -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
  • Tool使用

image.png

    • 获取工具列表
$ curl -X POST 'http://127.0.0.1:8080/message?sessionId=d8ffe4da-f2b9-400e-b548-7403869fb24e' -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","method":"tools/list","id":2}'
    • 工具调用
curl -X POST 'http://127.0.0.1:8080/message?sessionId=d8ffe4da-f2b9-400e-b548-7403869fb24e' -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"getAllBooks","arguments":{}},"id":3}'

streamableHttp

当前版本(0.9.0)的MCP SDK尚不支持streamableHttp,该种通信机制未验证。

参考资料