博客 / 詳情

返回

上下文協議(MCP)Java SDK 指南

當我們把各種內部系統、數據源、工具接入大語言模型時,往往會遇到一個尷尬的問題:每個團隊、每套系統都有自己的一套“接入規範”。有的用 HTTP API,有的用消息隊列,有的直接連數據庫,最後一圈串下來,既難以統一治理,又很難在不同應用之間複用。這時,你可能會問:有沒有一種通用的協議,既能讓 AI 模型方便地調用外部工具、訪問數據,又能讓後端服務方用標準方式暴露能力?

Model Context Protocol(MCP)就是為此而生的標準之一,而本文要介紹的 Java SDK,則為 Java 開發者提供了一條直接接入 MCP 生態的通路。通過它,你可以用統一的模型,在 Java 應用裏暴露工具、資源、提示模版,也可以輕鬆作為客户端去調用這些能力。本文將從整體架構講起,一步步帶你用一個可運行的示例,搭建起自己的 MCP 服務端與客户端。

1. 概覽

隨着近年來 AI 的快速發展,越來越多的工具和系統開始與 AI 模型集成。但隨之而來的一個挑戰是:每種集成都可能採用完全不同的標準和方式,將外部工具、資源和系統接入到 AI 模型中。

Model Context Protocol(MCP)是一個開源標準,它定義了 AI 應用(如大語言模型、圖像生成模型等)與工具、數據源以及其他資源之間的集成方式。藉助 MCP,AI 應用可以按外部系統約定的方式訪問數據、調用工具並執行工作流。

MCP 的 Java SDK 為開發者提供了一組庫,支持多種協議和通信機制,用於把 Java 應用與 AI 應用連接起來。

在本教程中,我們將一起了解這個 SDK,並通過一個簡單示例來體驗 MCP 的使用方式。

2. 架構

MCP 架構的核心組件主要包括:

  • MCP Host:負責管理多個 MCP Client
  • MCP Client:從 MCP Server 獲取上下文,供 MCP Host 使用
  • MCP Server:向 MCP Client 提供上下文信息和可調用能力

MCP 將通信劃分為兩個概念層次:數據層(Data Layer),用於定義客户端與服務端的通信協議和生命週期管理;以及 傳輸層(Transport Layer),用於定義客户端和服務端之間的具體傳輸通道和機制

Java 版的 MCP SDK 將這些概念映射為如下幾個層次:

  • Client/Server 層:通過 McpClient / McpServer 實現並管理客户端/服務端的具體操作
  • Session 層:通過 McpSession 管理通信模式和會話狀態
  • Transport 層:通過 McpTransport 處理消息的序列化與反序列化

客户端會調用 MCP 服務端暴露的一到多個工具(tool),而底層的通信則由傳輸層負責。

在 MCP 中,Primitive(原語) 是最基礎的構建單元,用來定義可用的上下文信息類型以及可執行的操作範圍。服務端和客户端都提供了一些原語。

服務端側的原語包括工具(tools)、資源(resources)和提示模版(prompts)。工具是 AI 應用可以調用的可執行函數,例如查詢數據庫、文件操作等。資源是提供給客户端的上下文數據源,例如數據庫結構、文件內容等。提示模版是可複用的模版,用於與語言模型進行交互

客户端側的原語則幫助 McpServer 的實現者構建更豐富的交互能力,包括採樣(sampling)、信息補充(elicitation)和日誌(logging)。採樣允許服務端在不集成模型 SDK 的情況下,向客户端請求語言模型補全結果。 信息補充讓服務端能夠向用户請求額外信息或確認操作。 日誌則允許服務端向客户端發送日誌消息,用於調試和監控。

3. 環境準備

要使用 MCP Java SDK,我們需要在項目中加入 mcp 依賴:

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

3.1 定義一個 MCP 工具

我們先通過 LoggingTool 這個類,定義一個非常簡單的 MCP 工具,用來打印收到的提示詞(prompt),該方法返回一個 SyncToolSpecification

public class LoggingTool {

    public static McpServerFeatures.SyncToolSpecification logPromptTool() {
        McpSchema.JsonSchema inputSchema = new McpSchema.JsonSchema("object",
            Map.of("prompt", String.class), List.of("prompt"), false, null, null);

        return new McpServerFeatures.SyncToolSpecification(
            new McpSchema.Tool(
              "logPrompt", "Log Prompt","Logs a provided prompt", inputSchema, null, null, null),
                (exchange, args) -> {
                    String prompt = (String) args.get("prompt");
                    return McpSchema.CallToolResult.builder()
                      .content(List.of(new McpSchema.TextContent("Input Prompt: " + prompt)))
                      .isError(false)
                      .build();
                });
    }
}

這裏我們首先定義了輸入的 JSON Schema,用來為用户輸入建立一個清晰的契約。接着,使用該輸入 Schema 來實例化一個 Tool,在處理邏輯中提取出 prompt 參數,並最終返回包含該 promptTextContent 結果。

4. MCP 客户端與服務端搭建

接下來,我們需要一個 MCP 服務端來暴露自定義工具,以及一個或多個 MCP 客户端,用於連接該服務端並調用其中的工具。

4.1 MCP 服務端實現

McpServer 具有一組能力(capabilities),用來告知客户端當前服務器支持哪些類別的協議操作,例如日誌記錄、提示詞補全、資源訪問等。此外,工具(tools)則提供給客户端可調用的具體函數。

先來看一個 McpServer 的實現:

public class McpServerApp {

    public static McpSyncServer createServer() {
        JacksonMcpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new ObjectMapper());
        StdioServerTransportProvider transportProvider = new StdioServerTransportProvider(
            jsonMapper);

        return McpServer.sync(transportProvider)
          .serverInfo("baeldung-demo-server", "0.0.1")
          .capabilities(McpSchema.ServerCapabilities.builder()
            .tools(true)
            .logging()
            .build())
          .tools(LoggingTool.logPromptTool())
          .build();
    }

    public static void main(String[] args) {
        createServer();
    }
}

上面代碼定義了一個同步的 McpServer,通過標準輸入/輸出流並使用 JSON 消息格式進行通信。隨後我們聲明瞭服務器的能力:開啓工具以及日誌功能(基於 SLF4J 日誌框架),最後把自定義的 logPromptTool 註冊到服務器上。

4.3 MCP 客户端實現

下面再定義一個簡單的 McpClient,用於連接到服務端:

public class McpClientApp {

    public static McpSyncClient getClient() {
        ServerParameters params = ServerParameters
          .builder("npx")
          .args("-y", "@modelcontextprotocol/server-everything")
          .build();

        JacksonMcpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new ObjectMapper());
        McpClientTransport transport = new StdioClientTransport(params, jsonMapper);

        return io.modelcontextprotocol.client.McpClient.sync(transport)
         .build();

    }

    public static void main(String[] args) {
        McpSyncClient client = getClient();
        client.initialize();
    }
}

這裏我們使用 MCP 提供的示例服務端,並通過 ServerParameters 進行配置。客户端同樣通過標準輸入/輸出流以及 JSON 消息格式與服務端進行同步通信。

5. 測試

到目前為止,我們已經具備了測試 MCP 交互和核心概念所需的全部組件。

5.1 測試 MCP 工具與客户端實現

我們先從測試 LoggingTool 開始,驗證其輸出是否正確:

@Test
void whenLogPromptToolCalled_thenReturnsResult() {
    McpSchema.CallToolRequest request = new McpSchema.CallToolRequest("",
        Map.of("prompt", "Unit test message"));

    McpServerFeatures.SyncToolSpecification toolSpec = LoggingTool.logPromptTool();
    McpSchema.CallToolResult result 
        = toolSpec.callHandler().apply(null, request);
    assertNotNull(result);
    assertFalse(result.isError());
    assertEquals(
        "Input Prompt: Unit test message",((McpSchema.TextContent) (result.content()
        .getFirst()))
        .text());
}

在這個測試中,我們創建了一個帶有 promptCallToolRequest,並將其傳遞給 LoggingToolSyncToolSpecification。隨後,我們斷言返回結果非空、無錯誤,並且返回的文本內容與預期相同。

接下來,我們再通過 MCP 提供的示例服務端來測試 McpClient

@Test
void whenCalledViaClient_thenReturnsLoggedResult() {
    McpSchema.CallToolRequest request = new McpSchema.CallToolRequest(
        "echo", Map.of("message", "Client-server test message"));
    McpSchema.CallToolResult result = client.callTool(request);

    assertNotNull(result);
    assertNull(result.isError());
    assertEquals("Echo: Client-server test message",
        ((McpSchema.TextContent) (result.content()
        .getFirst())).text());
}

MCP 示例服務端暴露了一個名為 echo 的工具,它會把輸入消息原樣返回,這與我們之前實現的 LoggingTool 的行為類似。

5.2 測試本地服務端

最後,我們來測試自己編寫的本地服務端。為此需要定義一個單獨的 McpClient,並使用指向本地 JAR 的不同服務端參數:

public class McpClientApp2 {

    private static final Logger log = LoggerFactory.getLogger(McpClientApp2.class);

    public static void main(String[] args) {
        String jarPath = new java.io.File("java-mcp/target/java-mcp-1.0.0-SNAPSHOT.jar")
                             .getAbsolutePath();
        ServerParameters params = ServerParameters.builder("java")
            .args("-jar", jarPath)
            .build();

        JacksonMcpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new ObjectMapper());
        McpClientTransport transport = new StdioClientTransport(params, jsonMapper);

        McpSyncClient client = McpClient.sync(transport)
            .build();

        client.initialize();

        ListToolsResult tools = client.listTools();
        McpClientApp2.log.info("Tools exposed by the server:");
        tools
          .tools()
          .forEach(tool -> System.out.println(" - " + tool.name()));

        McpClientApp2.log.info("\nCalling 'logPrompt' tool...");
        CallToolResult result = client.callTool(
          new CallToolRequest("logPrompt", Map.of("prompt", "Hello from MCP client!")));
        McpClientApp2.log.info("Result: " + result.content());

        client.closeGracefully();
    }
}

運行該客户端後,我們可以通過日誌來驗證它是否成功連接到了本地 JAR 中定義的服務端:

14:04:27.879 [boundedElastic-1] INFO  i.m.c.transport.StdioClientTransport - MCP server starting.
14:04:27.920 [boundedElastic-1] INFO  i.m.c.transport.StdioClientTransport - MCP server started
14:04:28.517 [pool-4-thread-1] INFO  i.m.c.transport.StdioClientTransport - STDERR Message received: 14:04:28.504 [pool-1-thread-1] INFO  i.m.server.McpAsyncServer - Client initialize request - Protocol: 2024-11-05, Capabilities: ClientCapabilities[experimental=null, roots=null, sampling=null, elicitation=null], Info: Implementation[name=Java SDK MCP Client, title=null, version=0.15.0]
14:04:28.575 [pool-1-thread-1] INFO  i.m.client.LifecycleInitializer - Server response with Protocol: 2024-11-05, Capabilities: ServerCapabilities[completions=null, experimental=null, logging=LoggingCapabilities[], prompts=null, resources=null, tools=ToolCapabilities[listChanged=true]], Info: Implementation[name=baeldung-demo-server, title=null, version=0.0.1] and Instructions null
14:04:28.626 [main] INFO  mcp.McpClientApp2 - Tools exposed by the server:
14:04:28.626 [main] INFO  mcp.McpClientApp2 - 
Calling 'logPrompt' tool...
 - logPrompt
14:04:28.671 [main] INFO  mcp.McpClientApp2 - Result: [TextContent[annotations=null, text=Input Prompt: Hello from MCP client!, meta=null]]
14:04:28.784 [ForkJoinPool.commonPool-worker-1] WARN  i.m.c.transport.StdioClientTransport - Process terminated with code 143

Process finished with exit code 0

從日誌可以看到,首先 McpServer 啓動成功,隨後客户端完成初始化並與服務端建立連接。接着客户端請求列出服務端暴露的工具,最後在調用 logPrompt 工具之後,我們也看到了來自服務端的返回結果。

6. 總結

在本文中,我們首先回顧了 MCP 及其 Java SDK 的整體架構,重點介紹了 McpServerMcpClientMcpHost 之間的關係,以及由 McpTransport 負責的傳輸層細節。

接着,我們瞭解了 MCP 中的各種原語(primitives),以及在服務端和客户端側分別可用的類型和能力。

最後,我們實現了一個簡單的 MCP 工具,並通過 MCP 示例服務端和本地自建服務端,驗證了 McpClient 的連接與調用流程。藉助 MCP 標準和 Java SDK,你可以更方便地把現有系統能力以統一的方式暴露給 AI 應用,也能在不同項目之間複用這套集成模式。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.