跳到主要内容

模型上下文协议(MCP)

LangChain4j 支持模型上下文协议(MCP),用于与可提供并执行工具的 MCP 兼容服务器通信。有关该协议的一般信息,请参见 MCP 官网

备注

想在 Java 中构建 MCP stdio 服务器? 服务器实现位于 LangChain4j Community。参见 构建 Java MCP stdio 服务器

协议规定了两种传输类型,这两种均受支持:

  • Streamable HTTP: 客户端发送 HTTP 请求,服务器以普通响应回复,或在需要随时间发送多个响应时 打开 SSE 流。
  • stdio:客户端 可将 MCP 服务器作为本地子进程运行,并通过标准输入/输出直接通信。

在规范之上,LangChain4j 还实现了 WebSocket 传输。该传输尚未标准化, 目前客户端的实现方式与 Quarkus MCP Server 扩展 实现的 WebSocket 传输兼容。 对于使用其他框架构建并暴露 WebSocket 的 MCP 服务器,兼容性不作保证。

此外,LangChain4J 支持 Docker stdio 传输,可用于以容器镜像分发的 stdio MCP 服务器。

LangChain4j 还支持旧版 HTTP/SSE 传输, 但该方式已弃用,将来会被移除。

要让聊天模型或 AI 服务运行 MCP 服务器提供的工具, 需要创建 MCP 工具提供者实例。

创建 MCP 工具提供者

MCP 传输

首先,需要一个 MCP 传输实例。

对于 stdio——以下示例展示如何从 NPM 包启动子进程服务器:

McpTransport transport = StdioMcpTransport.builder()
.command(List.of("/usr/bin/npm", "exec", "@modelcontextprotocol/server-everything@0.6.2"))
.logEvents(true) // only if you want to see the traffic in the log
.build();

对于 Streamable HTTP 传输,需要提供服务器 POST 端点的 URL:

McpTransport transport = StreamableHttpMcpTransport.builder()
.url("http://localhost:3001/mcp")
.logRequests(true) // if you want to see the traffic in the log
.logResponses(true)
.build();

注意: Streamable HTTP 传输可选择性地打开附属的 基于 GET 的 SSE 流, 用于接收服务器发起的通知和请求。在构建器上使用 .subsidiaryChannel(true) 启用。 默认禁用。若服务器不支持,传输会记录警告并继续运行而不使用该通道。 若流在建立后中断,传输会自动重连(遵循服务器的 retry 值,默认为 5 秒)。

对于 WebSocket 传输:

McpTransport transport = WebSocketMcpTransport.builder()
.url("ws://localhost:3001/mcp/ws")
.logResponses(true)
.logRequests(true)
.build();

对于旧版 HTTP 传输,有两个 URL:一个用于启动 SSE 通道,另一个用于通过 POST 提交命令。 后者由服务器动态提供,前者需使用 sseUrl 方法指定:

McpTransport transport = HttpMcpTransport.builder()
.sseUrl("http://localhost:3001/sse")
.logRequests(true) // if you want to see the traffic in the log
.logResponses(true)
.build();

对于 Docker stdio 传输,首先需要在 pom.xml 中添加模块:

<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp-docker</artifactId>
</dependency>

然后创建 Docker 传输:

McpTransport transport = DockerMcpTransport.builder()
.image("mcp/time")
.dockerHost("unix:///var/run/docker.sock")
.logEvents(true) // if you want to see the traffic in the log
.build();

MCP 客户端

从传输创建 MCP 客户端:

McpClient mcpClient = DefaultMcpClient.builder()
.key("MyMCPClient")
.transport(transport)
.build();

注意:客户端 key 是可选的,但建议设置,尤其是在存在多个 MCP 客户端、 需要加以区分时。

MCP 工具提供者

最后,从客户端创建 MCP 工具提供者:

McpToolProvider toolProvider = McpToolProvider.builder()
.mcpClients(mcpClient)
.build();

注意:一个 MCP 工具提供者可以同时使用多个客户端。 若使用此功能,还可通过 builder.failIfOneServerFails(boolean) 方法指定 从某一服务器检索工具失败时的行为。默认值为 false, 表示工具提供者将忽略某一服务器的错误并继续使用其他服务器。 若设为 true,任一服务器失败都会导致工具提供者抛出异常。

此外,MCP 服务器常常提供数十个工具,而给定的 AI 服务可能只需其中几个, 既为了防止使用不需要的工具,也为了降低幻觉的可能性。 McpToolProvider 允许按名称过滤这些工具,如下所示:

McpToolProvider toolProvider = McpToolProvider.builder()
.mcpClients(mcpClient)
.filterToolNames("get_issue", "get_issue_comments", "list_issues")
.build();

这样,配置了该 ToolProvider 的 AI 服务只能使用 上述 3 个工具,允许读取已有 issue,但阻止创建新 issue。 更一般地,ToolProvider 允许通过 BiPredicate<McpClient, ToolSpecification> 过滤工具。 当多个 MCP 客户端暴露同名且冲突的工具时,这也很有用。 例如,以下 ToolProvider 从两个 MCP 客户端获取工具, 但由于它们都有名为 echoInteger 的工具,因此仅取自 key 为 numeric-mcp 的 MCP 客户端:

McpToolProvider toolProvider = McpToolProvider.builder()
.mcpClients(mcpClient1, mcpClient2)
.filter((mcpClient, tool) ->
!tool.name().startsWith("echoInteger") ||
mcpClient.key().equals("numeric-mcp"))
.build();

注意:在同一个 McpToolProvider 构建器上多次调用 filter 方法, 将导致所有这些过滤器的合取(AND)。

为了允许应用在运行时连接或断开 MCP 服务器, 还可以向现有的 McpToolProvider 实例动态添加和移除客户端与过滤器。

要将工具提供者绑定到 AI 服务,只需使用 AI 服务构建器的 toolProvider 方法:

Bot bot = AiServices.builder(Bot.class)
.chatModel(model)
.toolProvider(toolProvider)
.build();

或者,可以使用 Map<ToolSpecification, ToolExecutor> 提供工具。

Map<ToolSpecification, ToolExecutor> tools = mcpClient.listTools().stream().collect(Collectors.toMap(
tool -> tool,
tool -> new McpToolExecutor(mcpClient)
));

要将工具绑定到 AI 服务,只需使用 AI 服务构建器的 tools 方法:

Bot bot = AiServices.builder(Bot.class)
.chatModel(model)
.tools(tools)
.build();

有关 LangChain4j 中工具支持的更多信息,请参见此处

MCP 工具名称映射

若使用多个 MCP 服务器,且它们暴露名称冲突的工具(或你只是想调整不合适的名称), 应用工具名称映射函数会很有用。 创建 McpToolProvider 时可指定 BiFunction<McpClient, ToolSpecification, String>

例如:

McpToolProvider toolProvider = McpToolProvider.builder()
.mcpClients(mcpClient1, mcpClient2)
.toolNameMapper((client, toolSpec) -> {
// Prefix all tool names with the name of the MCP client and an underscore
return client.key() + "_" + toolSpec.name();
})
.build();

之后,工具提供者返回的 ToolSpecification 对象将包含映射后的(逻辑)名称, 但生成的 ToolExecutor 在调用工具时会固定向服务器传递原始(物理)名称。

MCP 工具规范映射

与上述 MCP 工具名称映射类似,也可以映射完整的 ToolSpecification

McpToolProvider toolProvider = McpToolProvider.builder()
.mcpClients(mcpClient)
.toolSpecificationMapper((client, toolSpec) -> {
// Prefix all tool names with "myprefix_" and convert the description to uppercase
return toolSpec.toBuilder()
.name("myprefix_" + toolSpec.name())
.description(toolSpec.description().toUpperCase())
.build();
})
.build();

MCP 工具元数据

MCP 协议允许服务器以注解形式,或通过工具定义的 _meta 字段,为每个工具提供额外元数据。 LangChain4j 通过 ToolSpecification.metadata() 方法中存储的 map 暴露所有这些元数据。 注解使用 dev.langchain4j.mcp.client.McpToolMetadataKeys 类中可作为常量找到的键存储在 map 中。 _meta 字段的内容使用其原始键存储,JSON 值序列化为嵌套 map。

MCP 工具定义中直接存在的 title 字段暴露在元数据 map 的 McpToolMetadataKeys.TITLE 键下,以区别于从注解检索的 title——后者暴露在 McpToolMetadataKeys.ANNOTATION_TITLE 键下。

若工具有图标,它们暴露在元数据 map 的 McpToolMetadataKeys.ICONS 键下。

提供 _meta 字段

MCP 协议允许客户端向发送到服务器的每个请求和通知的 params 附加 _meta 对象。 这可用于传递 OpenTelemetry 追踪上下文、自定义应用元数据,或服务器可能需要的 任何其他带外信息。

要提供 _meta 字段,在客户端构建器上注册 McpMetaSupplier。 该供应商在每个请求或通知之前被调用,返回的 map 放入 params._meta。 与 HTTP 头不同,这适用于所有传输(stdio、HTTP、WebSocket)。

McpClient mcpClient = DefaultMcpClient.builder()
.transport(transport)
.metaSupplier(context -> Map.of(
"traceparent", "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01",
"custom-key", "custom-value"))
.build();

供应商接收一个可为空的 McpCallContext,其中包含正在发送的消息, 以及(在适用时)触发它的 AI 服务调用的 InvocationContext。 这允许供应商根据正在执行的操作改变元数据。

日志

MCP 协议还定义了服务器向客户端发送日志消息的方式。 默认情况下,客户端的行为是转换这些日志消息并使用 SLF4J 记录器记录它们。 若要更改此行为,有一个名为 dev.langchain4j.mcp.client.logging.McpLogMessageHandler 的接口,用作 接收到的日志消息的回调。若创建自己的 McpLogMessageHandler 实现, 将其传给 MCP 客户端构建器:

McpClient mcpClient = new DefaultMcpClient.Builder()
.transport(transport)
.logMessageHandler(new MyLogMessageHandler())
.build();

MCP 监听器

MCP 客户端支持在客户端生命周期内监听事件的监听器。 接口 dev.langchain4j.mcp.client.McpClientListener 作为监听器实现的基类。 单个客户端可注册多个监听器;它们都会在每次工具调用、 提示渲染和资源访问之前和之后被调用。调用监听器时会注入相应的 McpCallContext。该对象包含实际发送到服务器的 MCP 消息, 以及(在适用时——仅当此调用作为 AI 服务调用的一部分时)InvocationContext 实例。

可以逐个或批量添加监听器:

McpClient mcpClient = DefaultMcpClient.builder()
.transport(transport)
.addListener(new MyFirstListener())
.addListener(new MySecondListener())
.addListeners(List.of(new MyThirdListener(), new MyFourthListener()))
.build();

资源

有两种使用资源的方式。应用可以调用 MCP 客户端的资源相关方法以编程方式访问资源, 或者可以选择通过合成工具将资源自动暴露给 LLM 调用(一个工具用于获取资源列表, 另一个用于获取资源内容),以便聊天模型自行查阅资源。

以编程方式访问资源

要获取服务器上的 MCP 资源 列表, 使用 client.listResources(),资源模板则使用 client.listResourceTemplates()。 这将返回 McpResource 对象列表(或分别为 McpResourceTemplate)。这些对象包含资源的元数据,最重要的是 URI。

要获取资源的实际内容,使用 client.readResource(uri),并提供资源的 URI。 这会返回 McpReadResourceResult,其中包含 McpResourceContents 对象列表(单个 URI 上可能有多个资源内容, 例如当 URI 表示目录时)。每个 McpResourceContents 对象表示二进制 blob(McpBlobResourceContents) 或文本(McpTextResourceContents)。

通过合成工具自动暴露资源

若在构建 McpToolProvider 时使用构建器设置 McpResourcesAsToolsPresenter 实例, MCP 工具提供者会在其 provideTools 方法的结果中自动添加两个合成工具, 以及支撑 MCP 服务器支持的“常规”工具。一个工具用于获取资源列表,另一个用于获取特定资源。 LangChain4j 提供名为 DefaultMcpResourcesAsToolsPresenter 的默认实现,添加这两个工具:

注意: 本节其余部分描述 DefaultMcpResourcesAsToolsPresenter。你可以插入自己的实现, 其行为可能不同。

  • list_resources:列出支撑 MCP 服务器暴露的所有资源。该工具不接受参数。
  • get_resource:读取资源内容。该工具接受两个共同标识资源的参数: MCP 服务器名称和 URI。

list_resources 的输出是类似如下的 JSON 数组:

[ {
"mcpServer" : "alice",
"uri" : "file:///info",
"uriTemplate" : null,
"name" : "basicInfo",
"description" : "Basic information about Alice",
"mimeType" : "text/plain"
}, {
"mcpServer" : "bob",
"uri" : "file:///info",
"uriTemplate" : null,
"name" : "basicInfo",
"description" : "Basic information about Bob",
"mimeType" : "text/plain"
} ]

该数组中的每个文档代表一个资源。每个资源由 urimcpServer 的组合标识, 其中 mcpServer 是创建 MCP 客户端时分配的 key 值(参见 DefaultMcpClient.Builder#key)。 当聊天模型调用 list_resources 工具时, 它会收到此资源列表,然后可以决定调用 read_resourcelist_resourcesget_resource 工具的默认描述在大多数情况下足以向 LLM 解释如何使用它们。不过,若需要自定义这些工具及其参数的描述, 可使用 DefaultMcpResourcesAsToolsPresenter.Builder 的方法覆盖它们。

资源订阅

MCP 协议支持资源订阅, 允许客户端在服务器上的资源发生变化时收到通知。

要订阅特定资源的更新,使用 client.subscribeToResource(uri)。 当服务器更新资源时,会发送 notifications/resources/updated 通知。 要通过 onResourceUpdated 构建器方法注册回调来处理这些通知:

McpClient mcpClient = DefaultMcpClient.builder()
.transport(transport)
.onResourceUpdated((client, uri) -> {
// re-read the updated resource
McpReadResourceResult result = client.readResource(uri);
// process the updated contents...
})
.build();

// subscribe to a resource
mcpClient.subscribeToResource("file:///status");

// later, unsubscribe
mcpClient.unsubscribeFromResource("file:///status");

提示词

要获取服务器上的 MCP 提示词 列表, 使用 client.listPrompts()。该方法返回 McpPrompt 的 List。McpPrompt 包含有关提示词名称和参数的信息。

要渲染提示词的实际内容,使用 client.getPrompt(name, arguments)。渲染后的提示词可包含一到多条消息, 这些消息表示为 McpPromptMessage 对象。每个 McpPromptMessage 包含消息的角色(userassistant 等) 以及消息的实际内容。目前支持的消息内容类型为:McpTextContentMcpImageContentMcpEmbeddedResource

可以使用 McpPromptMessage.toChatMessage() 将其转换为 LangChain4j 核心 API 中的通用 dev.langchain4j.data.message.ChatMessage。但这并非在所有情况下都可行。例如,若提示词消息的 roleassistant 且包含非文本内容,将抛出异常。无论角色如何,都不支持将包含二进制 blob 内容的消息转换为 ChatMessage

通过 Docker 使用 GitHub MCP 服务器

接下来看看如何使用模型上下文协议(MCP),以标准化方式将 AI 模型与外部工具桥接。 以下示例将通过 LangChain4j MCP 客户端与 GitHub 交互,获取并总结公共 GitHub 仓库的最新提交。 为此,无需重新发明轮子,可以使用 MCP GitHub 仓库 中现有的 GitHub MCP 服务器实现

思路是构建一个 Java 应用,连接到在 Docker 中本地运行的 GitHub MCP 服务器,以获取并总结最新提交。 该示例使用 MCP 的 stdio 传输机制,在我们的 Java 应用与 GitHub MCP 服务器之间通信。

在 Docker 中打包并执行 GitHub MCP 服务器

要与 GitHub 交互,首先需要在 Docker 中设置 GitHub MCP 服务器。 GitHub MCP 服务器提供通过模型上下文协议与 GitHub 交互的标准化接口。 它支持文件操作、仓库管理和搜索功能。

要为我们的 GitHub MCP 服务器构建 Docker 镜像,需要从 MCP servers GitHub 仓库 获取代码,可通过克隆仓库或下载代码。 然后,导航到根目录并执行以下 Docker 命令:

docker build -t mcp/github -f src/github/Dockerfile .

Dockerfile 设置必要的环境并安装 GitHub MCP 服务器实现。 构建完成后,镜像将作为 mcp/github 在本地可用。

docker image ls

REPOSITORY TAG IMAGE ID SIZE
mcp/github latest b141704170b1 173MB

开发工具提供者

让我们创建一个名为 McpGithubToolsExample 的 Java 类,使用 LangChain4j 连接到我们的 GitHub MCP 服务器。该类将:

  • 在 Docker 容器中启动 GitHub MCP 服务器(docker 命令位于 /usr/local/bin/docker
  • 使用 stdio 传输建立连接
  • 使用 LLM 总结 LangChain4j GitHub 仓库的最近 3 次提交

注意:在下面的代码中,我们在环境变量 GITHUB_PERSONAL_ACCESS_TOKEN 中传递 GitHub token。但对某些不需要认证的公开仓库操作来说,这是可选的。

实现如下:

public static void main(String[] args) throws Exception {

ChatModel model = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.logRequests(true)
.logResponses(true)
.build();

McpTransport transport = new StdioMcpTransport.Builder()
.command(List.of("/usr/local/bin/docker", "run", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "-i", "mcp/github"))
.logEvents(true)
.build();

McpClient mcpClient = new DefaultMcpClient.Builder()
.transport(transport)
.build();

ToolProvider toolProvider = McpToolProvider.builder()
.mcpClients(List.of(mcpClient))
.build();

Bot bot = AiServices.builder(Bot.class)
.chatModel(model)
.toolProvider(toolProvider)
.build();

try {
String response = bot.chat("Summarize the last 3 commits of the LangChain4j GitHub repository");
System.out.println("RESPONSE: " + response);
} finally {
mcpClient.close();
}
}
备注

并非所有 LLM 都同样好地支持工具。 理解、选择和正确使用工具的能力在很大程度上取决于具体模型及其能力。 有些模型可能完全不支持工具,而另一些可能需要仔细的提示工程 或额外的系统指令。

注意:本示例使用 Docker,因此执行位于 /usr/local/bin/docker 的 Docker 命令(请根据操作系统更改路径)。若想使用 Podman 而非 Docker,请相应更改命令。

执行代码

要运行示例,请确保系统上的 Docker 已启动并运行。 同时,在环境变量 OPENAI_API_KEY 中设置你的 OpenAI API 密钥。

然后运行 Java 应用。你应该会得到总结 LangChain4j GitHub 仓库最近 3 次提交的响应,例如:

Here are the summaries of the last three commits in the LangChain4j GitHub repository:

1. **Commit [36951f9](https://github.com/langchain4j/langchain4j/commit/36951f9649c1beacd8b9fc2d910a2e23223e0d93)** (Date: 2025-02-05)
- **Author:** Dmytro Liubarskyi
- **Message:** Updated to `upload-pages-artifact@v3`.
- **Details:** This commit updates the GitHub Action used for uploading pages artifacts to version 3.

2. **Commit [6fcd19f](https://github.com/langchain4j/langchain4j/commit/6fcd19f50c8393729a0878d6125b0bb1967ac055)** (Date: 2025-02-05)
- **Author:** Dmytro Liubarskyi
- **Message:** Updated to `checkout@v4`, `deploy-pages@v4`, and `upload-pages-artifact@v4`.
- **Details:** This commit updates multiple GitHub Actions to their version 4.

3. **Commit [2e74049](https://github.com/langchain4j/langchain4j/commit/2e740495d2aa0f16ef1c05cfcc76f91aef6f6599)** (Date: 2025-02-05)
- **Author:** Dmytro Liubarskyi
- **Message:** Updated to `setup-node@v4` and `configure-pages@v4`.
- **Details:** This commit updates the `setup-node` and `configure-pages` GitHub Actions to version 4.

All commits were made by the same author, Dmytro Liubarskyi, on the same day, focusing on updating various GitHub Actions to newer versions.

不使用 AI Services 的 MCP

前面的示例展示了如何通过高层 AI Services API 使用 MCP。不过,也可以通过底层 API 使用 MCP。 你可以手动使用已构建的 DefaultMcpClient 实例对服务器执行命令。一些示例:

// obtain a list of tools from the server
List<ToolSpecification> toolSpecifications = mcpClient.listTools();

// build and execute a ChatRequest that has access to the MCP tools
ChatRequest chatRequest = ChatRequest.builder()
.messages(UserMessage.from("What will the weather be like in London tomorrow?"))
.toolSpecifications(toolSpecifications)
.build();
ChatResponse response = chatModel.chat(chatRequest);
AiMessage aiMessage = response.aiMessage();

// if the LLM requested to invoke a tool, forward it to the MCP server
if(aiMessage.hasToolExecutionRequests()) {
for (ToolExecutionRequest req : aiMessage.toolExecutionRequests()) {
String resultString = mcpClient.executeTool(req);
// prepare the result for adding it to the memory for the next ChatRequest...
ToolExecutionResultMessage resultMessage = ToolExecutionResultMessage.from(req.id(), req.name(), resultString);
}
}

若想直接以编程方式使用 MCP 客户端执行工具(在聊天之外), 需要手动构建 ToolExecutionRequest 实例:

// to execute a tool named "tool1" with argument "a=b"
ToolExecutionRequest request = ToolExecutionRequest.builder()
.name("tool1")
.arguments("{\"a\": \"b\"}")
.build();
String toolResult = mcpClient.executeTool(request);

关于工具缓存的说明

DefaultMcpClient 维护 MCP 工具的内部缓存。一旦检索到, 除非服务器发送列表已更新的通知,否则不会再次从 MCP 服务器请求工具列表。 你可以通过调用 DefaultMcpClient.evictToolListCache() 手动清除此缓存。 若希望完全禁用缓存,按如下方式配置客户端:

McpClient mcpClient = new DefaultMcpClient.Builder()
.key("MyMCPClient")
.transport(transport)
.cacheToolList(false)
.build();

MCP 注册表客户端

LangChain4j 还提供可与 MCP 注册表 通信的独立客户端实现。目前仅实现了只读操作(可以搜索 MCP 服务器,但不支持管理和添加服务器—— 请使用官方工具完成这些操作)。

警告: 发现 MCP 服务器并使用它们(尤其是本地运行)可能带来严重的安全风险。在运行 你在公共注册表中找到的任何 MCP 服务器之前,请确保可以信任它。

注册表客户端位于 dev.langchain4j.mcp.registryclient 包中,可按如下方式初始化:

McpRegistryClient client = DefaultMcpRegistryClient.builder()
.baseUrl("URL-OF-THE-REGISTRY")
.build();

若未提供 base URL,将默认使用官方注册表(https://registry.modelcontextprotocol.io)。 然后,使用 registry.listServers(McpServerListRequest) 方法搜索 MCP 服务器。可使用 McpServerListRequest.Builder 类构建 McpServerListRequest 对象。LangChain4j 中的 Java API 与官方 MCP Registry Reference 中描述的 MCP 注册表 REST API 密切对应。