欢迎光临
我们一直在努力

LangChain4j Java AI 应用开发实战(二十八):MCP 协议 —— 安全的工具与数据源接入标准

目录

  • 前言
  • 一、MCP 协议核心概念
    • 1.1 什么是 MCP 协议?
    • 1.2 MCP 协议的核心能力
    • 1.3 MCP vs 自定义 @Tool:何时该用 MCP?
  • 二、MCP 的两种传输模式
    • 2.1 Stdio 传输(标准输入输出)
    • 2.2 HTTP/SSE 传输
    • 2.3 两种传输模式对比
  • 三、环境准备与依赖配置
    • 3.1 核心依赖
    • 3.2 Spring Boot 配置
  • 四、实战案例一:基于 Stdio 的文件读取 Bot
    • 4.1 前置准备
    • 4.2 定义 AI Service 接口
    • 4.3 完整实现:Step by Step
    • 4.4 关键代码详解
      • 🔑 Step 1:命令构建 —— 跨平台考量
      • 🔒 Step 2:白名单目录 —— 安全第一
      • 🔗 Step 3:StdioMcpTransport 的工作原理
      • 📦 Step 4:McpToolProvider —— MCP 与 LangChain4j 的桥梁
      • 🧹 Step 5:Finally 块 —— 确保资源清理
  • 五、实战案例二:基于 Stdio 的百度地图查询助手
    • 5.1 社区 MCP Server 生态
    • 5.2 Spring Boot 集成实现
    • 5.3 环境变量传递
  • 六、实战案例三:基于 HTTP/SSE 的远程工具调用
    • 6.1 前置准备
    • 6.2 完整实现
    • 6.3 HTTP vs Stdio 的关键差异
  • 七、MCP 安全模型深度解析
    • 7.1 白名单机制(目录级沙箱)
    • 7.2 环境变量隔离
    • 7.3 工具级权限控制
    • 7.4 审计日志
    • 7.5 安全最佳实践清单
  • 八、MCP 生态与社区
    • 8.1 官方 MCP Server 列表
    • 8.2 社区 MCP Server
    • 8.3 自己搭建 MCP Server
  • 九、常见问题与避坑指南
    • 9.1 Windows 上 npx 无法执行
    • 9.2 工作目录不正确导致路径错误
    • 9.3 模型不支持 Function Calling
    • 9.4 子进程成为僵尸进程
    • 9.5 HTTP 传输的 SSE 连接超时
  • 十、进阶技巧与最佳实践
    • 10.1 多 MCP Server 聚合
    • 10.2 MCP 与 @Tool 混合使用
    • 10.3 Spring Boot 环境下的 McpClient 生命周期管理
    • 10.4 错误处理与降级
  • 十一、总结与展望
    • 本文核心要点回顾
    • 适用场景速查
  • 延伸阅读

前言

在前面《Function Calling 工具调用》中,我们学习了如何通过 @Tool 注解将 Java 方法暴露给大模型调用。这种方式的优点是简单直观,但也有一个明显的局限:

场景:被集成方不是 Java

你的团队(Java):
构建了一个智能助手,需要集成两个外部工具:

🛠️ 工具 A:内部数据库查询服务
→ 对方团队用 Python 开发,提供了 REST API

🛠️ 工具 B:文件系统浏览服务
→ GitHub 上已有的开源项目,用 TypeScript 实现

传统 @Tool 方案的问题:
❌ 必须用 Java 重写工具的调用逻辑(重复造轮子)
❌ 工具的实现语言、部署方式各不相同,集成成本高
❌ 每个工具的认证、权限、输入校验都需要自己实现
❌ 没有统一的标准来管理工具的"发现 → 调用 → 监控"全流程

你的 AI 助手的真实需求:

传统方案: 理想方案:
┌──────────────┐ ┌──────────────┐
│ Java 应用 │ │ Java 应用 │
│ ┌──────────┐ │ │ ┌──────────┐ │
│ │ @Tool A │─┼──→ REST → Python │ │ MCPClient │─┼──→ MCP Server A
│ ├──────────┤ │ │ ├──────────┤ │ (Python)
│ │ @Tool B │─┼──→ REST → TS │ │ MCPClient │─┼──→ MCP Server B
│ └──────────┘ │ │ └──────────┘ │ (TypeScript)
│ 每个工具需要 │ │ 统一协议, │
│ 单独适配! │ │ 即插即用! │
└──────────────┘ └──────────────┘

MCP(Model Context Protocol)协议正是为解决这个问题而生的!

MCP 是由 Anthropic 提出的开放标准协议,它定义了 LLM 应用与外部工具、数据源之间的标准化通信方式。通过 MCP,你的 Java 应用可以用同一种方式集成任何语言实现的工具服务器——就像 USB-C 统一了各种外设的接口一样,MCP 统一了大模型与外部世界的连接方式。

LangChain4j 从 1.14.0 版本开始提供了 langchain4j-mcp 模块,让 Java 开发者可以轻松地作为 MCP 客户端消费远程工具,同时也能构建 MCP 服务器对外暴露能力。


一、MCP 协议核心概念

1.1 什么是 MCP 协议?

MCP(Model Context Protocol)是一种客户端-服务器架构的开放协议,用于在 LLM 应用与外部工具/数据源之间建立标准化通信。

┌──────────────────────────────────────────────────────────────┐
│ MCP 生态全景 │
│ │
│ ┌────────────────────┐ ┌────────────────────┐ │
│ │ Java 应用 │ │ Python 应用 │ │
│ │ (LangChain4j) │ │ (LangChain) │ │
│ │ │ │ │ │
│ │ ┌──────────────┐ │ │ ┌──────────────┐ │ │
│ │ │ MCP Client │ │ │ │ MCP Client │ │ │
│ │ └──────┬───────┘ │ │ └──────┬───────┘ │ │
│ └─────────┼──────────┘ └─────────┼──────────┘ │
│ │ │ │
│ │ MCP Protocol │ │
│ │ ┌──────────────────────┐ │ │
│ └──┤ • 工具发现 (list) ├─────┘ │
│ │ • 工具调用 (call) │ │
│ │ • 资源访问 (read) │ │
│ │ • 提示模板 (prompt) │ │
│ │ • 安全协商 (auth) │ │
│ └──────────┬───────────┘ │
│ │ │
│ ┌──────────────┼──────────────┐ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ MCP Server │ │ MCP Server │ │ MCP Server │ … │
│ │ (Filesystem)│ │ (Database) │ │ (BaiduMap) │ │
│ │ Node.js │ │ Python │ │ TypeScript │ │
│ └────────────┘ └────────────┘ └────────────┘ │
└──────────────────────────────────────────────────────────────┘

MCP 协议的核心价值:

维度传统方式MCP 方式
工具集成 每个工具单独适配,需用 Java 重写 统一协议,任何语言实现的 Server 都能即插即用
服务发现 硬编码工具列表 客户端自动从 Server 获取工具清单
安全控制 需要自己实现权限、校验、审计 协议内置:白名单目录、环境变量隔离、审计日志
跨语言 Java 调用 Python 需要 REST/gRPC 桥梁 基于标准 I/O 或 HTTP,天然跨语言
生态复用 工具无法跨项目共享 社区 MCP Server 市场,安装即用
生命周期 需自己管理外部进程启停 MCP Client 自动管理子进程/连接生命周期

1.2 MCP 协议的核心能力

MCP 协议定义了四大核心原语:

┌─────────────────────────────────────────────────┐
│ MCP 四大核心原语 │
│ │
│ 1️⃣ Tools(工具) │
│ • 让 LLM 通过 MCP Server 执行操作 │
│ • 类比:Function Calling 的标准化版本 │
│ • 示例:读文件、查数据库、调 API │
│ │
│ 2️⃣ Resources(资源) │
│ • 让 LLM 通过 MCP Server 读取数据 │
│ • 类比:RAG 中 DocumentLoader 的标准化版本 │
│ • 示例:文件内容、数据库记录、API 响应 │
│ │
│ 3️⃣ Prompts(提示模板) │
│ • 让 Server 提供预定义的提示模板 │
│ • 类比:PromptTemplate 的远程版本 │
│ • 示例:代码审查模板、客服话术模板 │
│ │
│ 4️⃣ Sampling(采样) │
│ • Server 反向请求 Client 进行 LLM 推理 │
│ • 适用于 Agent 嵌套调用的场景 │
│ • 示例:Server 内部需要 LLM 做中间决策 │
└─────────────────────────────────────────────────┘

目前 LangChain4j 的 MCP 模块已经完整支持 Tools 原语,这也是本文的重点。

1.3 MCP vs 自定义 @Tool:何时该用 MCP?

这是一个非常重要的决策问题。下面是决策指南:

是否需要使用 MCP?


┌──────────────────┐
│ 工具逻辑是否已经在 │
│ 其他语言中实现? │
└──────┬───────────┘

┌───┴───┐
│ 是 │ 否
▼ ▼
✅ MCP ┌──────────────────┐
│ 工具是否需要独立 │
│ 部署/扩容? │
└──────┬───────────┘

┌───┴───┐
│ 是 │ 否
▼ ▼
✅ MCP ┌──────────────────┐
│ 工具是否要被多个 │
│ 应用共享复用? │
└──────┬───────────┘

┌───┴───┐
│ 是 │ 否
▼ ▼
✅ MCP 📌 @Tool 即可

简单总结:

  • 📌 用 @Tool:工具逻辑简单、纯 Java 实现、不需要独立部署、仅在一个应用中使用
  • ✅ 用 MCP:工具已用其他语言实现、需要独立部署/扩容、被多个应用共享、来自社区生态

二、MCP 的两种传输模式

MCP 协议支持两种底层的传输模式,适用于不同的部署场景:

2.1 Stdio 传输(标准输入输出)

┌─────────────────────────────────────────────┐
│ Stdio 传输模式架构 │
│ │
│ ┌──────────────────┐ │
│ │ Java 进程 │ │
│ │ ┌──────────────┐ │ stdin/stdout │
│ │ │ MCP Client │◄┼─────────────────────►│
│ │ │ (Stdio │ │ 子进程通信 │
│ │ │ Transport) │ │ │
│ │ └──────────────┘ │ │
│ │ │ │ │
│ │ │ Runtime │ │
│ │ │ .exec() │ │
│ │ ▼ │ │
│ │ ┌──────────┐ │ │
│ │ │ 子进程 │ │ │
│ │ │ (MCP │ │ │
│ │ │ Server) │ │ │
│ │ └──────────┘ │ │
│ └──────────────────┘ │
│ │
│ 特点: │
│ ✅ 无需网络端口,进程隔离更安全 │
│ ✅ 适合本地工具(文件系统、命令行、数据库) │
│ ✅ 启动/关闭生命周期由父进程管理 │
│ ⚠️ 仅限本地通信,无法远程调用 │
│ ⚠️ 每个 MCP Server 一个子进程 │
└─────────────────────────────────────────────┘

Stdio 传输的核心机制:

  • Java 进程通过 Runtime.exec() 或 ProcessBuilder 启动 MCP Server 作为子进程
  • 通过子进程的 stdin 发送 JSON-RPC 格式的 MCP 请求
  • 通过子进程的 stdout 接收 MCP 响应
  • stderr 用于 MCP Server 自身的日志输出
  • 父进程退出时自动清理子进程

适用场景:

  • 本地文件系统操作(读写文件、遍历目录)
  • 本地命令行工具封装(Git 操作、系统监控)
  • 本地数据库访问(SQLite、本地 MySQL)
  • 任何不需要远程调用的工具

2.2 HTTP/SSE 传输

┌─────────────────────────────────────────────┐
│ HTTP/SSE 传输模式架构 │
│ │
│ ┌──────────────────┐ │
│ │ Java 进程 │ HTTP POST │
│ │ ┌──────────────┐ │─────────────────────►│
│ │ │ MCP Client │ │ (JSON-RPC 请求) │
│ │ │ (Http │ │ │
│ │ │ Transport) │◄─────────────────────┤
│ │ └──────────────┘ │ SSE (服务端推送) │
│ │ │ │
│ └──────────────────┘ │
│ │
│ │
│ ┌──────────────┐ │
│ │ MCP Server │ │
│ │ (HTTP + SSE) │ │
│ │ localhost: │ │
│ │ 3001/sse │ │
│ └──────────────┘ │
│ │
│ 特点: │
│ ✅ 支持跨机器远程调用 │
│ ✅ 可以多个客户端共享同一个 Server │
│ ✅ 适合微服务架构 │
│ ⚠️ 需要管理网络连接和认证 │
│ ⚠️ 需要独立部署和运维 Server │
└─────────────────────────────────────────────┘

HTTP/SSE 传输的核心机制:

  • 客户端通过 HTTP POST 向 Server 发送 JSON-RPC 请求
  • Server 通过 SSE(Server-Sent Events)端点向客户端推送响应和通知
  • 支持超时控制、断线重连
  • 可以添加 TLS/HTTPS 进行加密通信

适用场景:

  • 远程 API 调用(天气查询、地图服务、股票数据)
  • 微服务架构中的工具服务
  • 需要多客户端共享的工具
  • 需要独立扩容的工具服务

2.3 两种传输模式对比

维度Stdio 传输HTTP/SSE 传输
通信范围 仅限本机进程间 支持跨网络
网络依赖 无需网络端口 需要 TCP 端口
安全性 最高(进程隔离) 需要认证 + TLS
启动方式 Java 父进程自动启动 需要预先启动 Server
生命周期 同父进程生命周期 独立生命周期
多客户端 不支持(1对1) 支持(1对多)
延迟 极低(进程内通信) 较低(localhost)/中等(远程)
运维复杂度 中等
扩展性

选择建议:

  • 🖥️ 本地工具(文件、命令行、本地数据库)→ Stdio
  • 🌐 远程服务(API、微服务、SaaS)→ HTTP/SSE
  • 🔒 高安全场景(敏感数据、内网环境)→ Stdio
  • 📈 高并发场景(多客户端共享)→ HTTP/SSE

三、环境准备与依赖配置

3.1 核心依赖

在开始实战之前,先了解 MCP 相关的 Maven 依赖:

纯 Java 项目(langchain4j-04-mcp-examples):

<dependencyManagement>
<dependencies>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-bom</artifactId>
<version>1.14.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<!– MCP 模块:提供 McpToolProvider、McpClient、传输层 –>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</artifactId>
</dependency>

<!– 模型依赖(以 OpenAI 为例) –>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
</dependency>
</dependencies>

Spring Boot 项目(langchain4j-spring-boot-10-mcp):

<dependencies>
<!– Spring Boot Starter –>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
</dependency>

<!– OpenAI Spring Boot Starter(自动配置 ChatModel Bean) –>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
</dependency>

<!– MCP 模块 –>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</artifactId>
</dependency>

<!– 流式响应支持 –>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-reactor</artifactId>
</dependency>
</dependencies>

📦 关键依赖说明:

  • langchain4j-mcp:包含 McpToolProvider、DefaultMcpClient、StdioMcpTransport、HttpMcpTransport 等核心类
  • MCP 依赖 LangChain4j 1.14.0+ 版本,低版本不支持

3.2 Spring Boot 配置

在 application.properties 中配置 ChatModel(以 DeepSeek 为例):

# 应用基础配置
spring.application.name=LangChain4j-Spring-Boot
server.port=8082

# OpenAI 兼容的 ChatModel 配置(DeepSeek)
langchain4j.open-ai.chat-model.api-key=${DEEPSEEK_API_KEY}
langchain4j.open-ai.chat-model.base-url=https://api.deepseek.com
langchain4j.open-ai.chat-model.model-name=deepseek-chat
langchain4j.open-ai.chat-model.log-requests=true
langchain4j.open-ai.chat-model.log-responses=true

# 流式模型配置(可选)
langchain4j.open-ai.streaming-chat-model.api-key=${DEEPSEEK_API_KEY}
langchain4j.open-ai.streaming-chat-model.base-url=https://api.deepseek.com
langchain4j.open-ai.streaming-chat-model.model-name=deepseek-chat

⚠️ 注意:MCP 要求底层 ChatModel 支持 Function Calling。大多数主流模型(GPT-4o、GPT-4o-mini、DeepSeek-chat)都支持,但部分轻量模型可能不支持。


四、实战案例一:基于 Stdio 的文件读取 Bot

第一个案例我们将使用 MCP 官方的 @modelcontextprotocol/server-filesystem 服务器,让 AI 助手能够安全地读取本地文件。

4.1 前置准备

这个 MCP Server 由 Anthropic 官方维护,提供文件系统操作工具(read_file、list_directory 等)。

需要安装:

  • Node.js + npm(MCP Server 运行环境)

核心原理:

用户提问:"读取 file.txt 的内容"


┌──────────────┐
│ AI Service │ 检测到需要读取文件
│ (Bot) │
└──────┬───────┘
│ 调用工具

┌──────────────┐
│ McpTool │ 将调用委托给 MCP Client
│ Provider │
└──────┬───────┘


┌──────────────┐ 通过 stdin 发送 JSON-RPC 请求
│ StdioMcp │ {"method":"tools/call","params":{"name":"read_file",…}}
│ Transport │
└──────┬───────┘
│ stdin/stdout

┌──────────────┐
│ MCP Server │ 执行文件读取,返回内容
│ (子进程) │
└──────────────┘

4.2 定义 AI Service 接口

package dev.langchain4j.example.mcp;

public interface Bot {
String chat(String prompt);
}

这个接口极其简洁——就是一次普通的对话。但当我们通过 McpToolProvider 注入 MCP 工具后,Bot 将自动获得文件系统操作能力。

4.3 完整实现:Step by Step

以下是 McpToolsExampleOverStdio.java 的完整实现,我们将它拆解为六个步骤:

package dev.langchain4j.example.mcp;

import dev.langchain4j.mcp.McpToolProvider;
import dev.langchain4j.mcp.client.DefaultMcpClient;
import dev.langchain4j.mcp.client.McpClient;
import dev.langchain4j.mcp.client.transport.McpTransport;
import dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.tool.ToolProvider;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

public class McpToolsExampleOverStdio {

public static final String FILE_TO_BE_READ = "src/main/resources/file.txt";

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

// ============ Step 1: 配置大语言模型 ============
ChatModel model = OpenAiChatModel.builder()
.apiKey("demo") // 演示 API Key
.modelName("gpt-4o-mini") // 支持 Function Calling
.baseUrl("http://langchain4j.dev/demo/openai/v1")
.build();

// ============ Step 2: 构建 Stdio 启动命令 ============
boolean isWindows = System.getProperty("os.name")
.toLowerCase().contains("win");

List<String> command = new ArrayList<>();
if (isWindows) {
command.add("cmd"); // Windows 需要 cmd /c 包装
command.add("/c");
command.add("npx"); // npx 执行 npm 包
} else {
command.add("npx");
}

command.add("-y"); // 自动确认安装
command.add("@modelcontextprotocol/server-filesystem@0.6.2");
command.add(new File("src/main/resources")
.getAbsolutePath()); // 白名单目录(安全关键!)

// ============ Step 3: 创建 Stdio 传输层 ============
McpTransport transport = new StdioMcpTransport.Builder()
.command(command) // 子进程启动命令
.logEvents(true) // 打印通信事件(调试用)
.build();

// ============ Step 4: 创建 MCP 客户端 ============
McpClient mcpClient = new DefaultMcpClient.Builder()
.transport(transport) // 绑定传输层
.build();

// ============ Step 5: 包装为 ToolProvider ============
ToolProvider toolProvider = McpToolProvider.builder()
.mcpClients(List.of(mcpClient))
.build();

// ============ Step 6: 构建 AI Service 并对话 ============
Bot bot = AiServices.builder(Bot.class)
.chatModel(model)
.toolProvider(toolProvider) // 注入 MCP 工具
.build();

try {
File file = new File(FILE_TO_BE_READ);
String response = bot.chat(
"Read the contents of the file "
+ file.getAbsolutePath()
);
System.out.println("RESPONSE: " + response);
} finally {
mcpClient.close(); // 确保子进程被清理
}
}
}

4.4 关键代码详解

🔑 Step 1:命令构建 —— 跨平台考量

// Linux/macOS:
// npx -y @modelcontextprotocol/server-filesystem@0.6.2 /path/to/resources

// Windows:
// cmd /c npx -y @modelcontextprotocol/server-filesystem@0.6.2 C:\\path\\to\\resources

boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win");
List<String> command = new ArrayList<>();
if (isWindows) {
command.add("cmd");
command.add("/c");
command.add("npx");
} else {
command.add("npx");
}
command.add("-y");
command.add("@modelcontextprotocol/server-filesystem@0.6.2");

为什么需要这个判断?

在 Windows 上,npx 实际是 npx.cmd 批处理脚本。如果不通过 cmd /c 包装,Java 的 Runtime.exec() 无法直接执行 .cmd 文件。这是 Windows 开发中非常容易踩的坑。

最佳实践:

  • ✅ 使用 "npx" 而非硬编码 /usr/bin/npm(跨平台 PATH 查找)
  • ✅ 用 System.getProperty("os.name") 动态判断平台
  • ✅ 使用 List<String> 而非字符串拼接(参数含空格时更安全)
  • 🔒 Step 2:白名单目录 —— 安全第一

    command.add(new File("src/main/resources").getAbsolutePath());

    这是 MCP 安全模型的核心!

    @modelcontextprotocol/server-filesystem 要求启动时指定一个或多个允许访问的目录。MCP Server 只会操作这些白名单目录内的文件,无法访问系统的其他部分:

    MCP Server 文件访问范围:

    允许: 拒绝:
    ✅ src/main/resources/file.txt ❌ /etc/passwd
    ✅ src/main/resources/docs/ ❌ ~/.ssh/id_rsa
    ✅ src/main/resources/../ ❌ ../../../
    (白名单内的相对路径) (跳出白名单的路径)

    这种设计类似于 Docker 的 Volume 挂载——你显式声明哪些目录对容器可见。即使 AI “要求” MCP Server 读取 /etc/passwd,Server 也会拒绝,因为该路径不在白名单中。

    🔗 Step 3:StdioMcpTransport 的工作原理

    McpTransport transport = new StdioMcpTransport.Builder()
    .command(command)
    .logEvents(true)
    .build();

    当你调用 .build() 时,StdioMcpTransport 内部会执行:

    1. ProcessBuilder pb = new ProcessBuilder(command);
    2. Process process = pb.start();
    3. 获取 process.getOutputStream() → 用于发送 MCP 请求(写入子进程 stdin)
    4. 获取 process.getInputStream() → 用于接收 MCP 响应(读取子进程 stdout)
    5. 获取 process.getErrorStream() → 用于接收 Server 日志(读取子进程 stderr)

    .logEvents(true) 的作用:开启后会打印所有 stdin/stdout 通信内容,对调试非常有帮助:

    [STDIO] → {"jsonrpc":"2.0","method":"tools/list","id":1}
    [STDIO] ← {"jsonrpc":"2.0","result":{"tools":[…]},"id":1}
    [STDIO] → {"jsonrpc":"2.0","method":"tools/call","params":{…},"id":2}
    [STDIO] ← {"jsonrpc":"2.0","result":{"content":[{"type":"text","text":"Kaboom!"}]},"id":2}

    ⚠️ 生产环境建议关闭 logEvents,避免日志量过大。

    📦 Step 4:McpToolProvider —— MCP 与 LangChain4j 的桥梁

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

    McpToolProvider 是 LangChain4j 与 MCP 之间的关键桥梁:

    ┌───────────────────────────────────────────────────┐
    │ McpToolProvider 桥梁作用 │
    │ │
    │ AiServices │
    │ │ 需要工具列表 │
    │ ▼ │
    │ ┌──────────────────────┐ │
    │ │ McpToolProvider │ │
    │ │ │ │
    │ │ 1. 从 MCP Client │ │
    │ │ 获取工具列表 │──── MCP Client ────► MCP Server
    │ │ │ tools/list │
    │ │ 2. 将 MCP 工具描述 │ │
    │ │ 转换为 ToolSpec │ │
    │ │ │ │
    │ │ 3. 执行工具调用时 │ │
    │ │ 委托给 MCP Client │──── MCP Client ────► MCP Server
    │ │ │ tools/call │
    │ └──────────────────────┘ │
    └───────────────────────────────────────────────────┘

    支持多个 MCP Client:你可以连接多个 MCP Server,它们的工具会自动聚合:

    ToolProvider toolProvider = McpToolProvider.builder()
    .mcpClients(List.of(
    filesystemClient, // 文件系统工具
    databaseClient, // 数据库工具
    baiduMapClient // 百度地图工具
    ))
    .build();

    // AI Service 将同时拥有以上所有工具!

    🧹 Step 5:Finally 块 —— 确保资源清理

    try {
    // … 对话逻辑
    } finally {
    mcpClient.close();
    }

    mcpClient.close() 对于 Stdio 传输至关重要:

  • 向子进程发送关闭信号
  • 如果子进程未响应,调用 destroyForcibly() 强制终止
  • 关闭 stdin/stdout/stderr 管道
  • 如果不调用 close():子进程会变为僵尸进程,持续占用系统资源。


    五、实战案例二:基于 Stdio 的百度地图查询助手

    第二个案例展示如何通过社区 MCP Server 接入外部 API 服务——百度地图。这个案例来自 Spring Boot 模块 langchain4j-spring-boot-10-mcp。

    5.1 社区 MCP Server 生态

    除了官方 Server,社区贡献了大量的 MCP Server,覆盖各种服务:

    MCP Server功能安装方式
    @baidumap/mcp-server-baidu-map 百度地图(地理编码、逆地理编码、天气查询) npx -y @baidumap/mcp-server-baidu-map
    @modelcontextprotocol/server-filesystem 文件系统操作 npx -y @modelcontextprotocol/server-filesystem
    @modelcontextprotocol/server-postgres PostgreSQL 数据库 npx -y @modelcontextprotocol/server-postgres
    @modelcontextprotocol/server-github GitHub API 官方维护
    @anthropic/mcp-server-brave-search Brave 搜索引擎 官方维护
    @modelcontextprotocol/server-everything 测试/参考实现 官方维护

    5.2 Spring Boot 集成实现

    在 Spring Boot 项目中,ChatModel 由 Spring 容器管理,我们通过 @Autowired 注入即可。

    定义 Assistant 接口(Spring Boot 方式):

    package com.langchain4j;

    import dev.langchain4j.service.SystemMessage;
    import dev.langchain4j.service.spring.AiService;
    import reactor.core.publisher.Flux;

    import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;

    @AiService(wiringMode = EXPLICIT,
    chatModel = "openAiChatModel",
    streamingChatModel = "openAiStreamingChatModel"
    )
    public interface Assistant {

    @SystemMessage("你是一个AI智能助手")
    String chat(String message);
    }

    测试代码:

    @SpringBootTest
    public class McpTest {

    @Autowired
    private OpenAiChatModel openAiChatModel;

    @Test
    public void testStudioBaiduMap() throws Exception {
    // Step 1: 构建 Stdio 传输层
    McpTransport transport = new StdioMcpTransport.Builder()
    .command(List.of(
    "D:/Program Files/nodejs/npx.cmd", // npx 绝对路径
    "-y",
    "@baidumap/mcp-server-baidu-map"
    ))
    .environment(Map.of(
    "BAIDU_MAP_API_KEY", "your_api_key_here"
    ))
    .logEvents(true)
    .build();

    // Step 2-4: 创建 MCP Client → ToolProvider → AI Service
    McpClient mcpClient = new DefaultMcpClient.Builder()
    .transport(transport)
    .build();

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

    Assistant assistant = AiServices.builder(Assistant.class)
    .chatModel(openAiChatModel)
    .toolProvider(toolProvider)
    .build();

    // Step 5: 对话
    String response = assistant.chat("查询一下深圳今天的天气");
    System.out.println("response: " + response);

    mcpClient.close();
    }
    }

    5.3 环境变量传递

    注意这个关键配置:

    .environment(Map.of(
    "BAIDU_MAP_API_KEY", "your_api_key_here"
    ))

    .environment() 方法允许你向 MCP Server 子进程传递环境变量。这是一个重要的安全设计:

    为什么通过环境变量传递 API Key?


    ┌──────────────────────────────────────────┐
    │ 1. 不在命令行参数中暴露(避免 ps -ef 泄露)│
    │ 2. 不在代码中硬编码(避免提交到 Git) │
    │ 3. MCP Server 子进程的环境变量是隔离的 │
    │ (不影响父进程的环境变量) │
    └──────────────────────────────────────────┘

    生产环境最佳实践:

    // ❌ 不推荐:硬编码 API Key
    .environment(Map.of("BAIDU_MAP_API_KEY", "dD2T4TUrZgl4OmmqkjGhuV1iNqggrYP3"))

    // ✅ 推荐:从环境变量或配置中心读取
    .environment(Map.of("BAIDU_MAP_API_KEY",
    System.getenv("BAIDU_MAP_API_KEY")))

    // ✅ 更推荐:使用 Spring 配置
    @Value("${baidu.map.api-key}")
    private String baiduMapApiKey;
    // …
    .environment(Map.of("BAIDU_MAP_API_KEY", baiduMapApiKey))


    六、实战案例三:基于 HTTP/SSE 的远程工具调用

    第三个案例展示 HTTP/SSE 传输模式。这种方式适合调用远程部署的 MCP Server。

    6.1 前置准备

    首先需要启动一个 HTTP 模式的 MCP Server。以官方 everything 测试服务器为例:

    # 克隆官方的 servers 仓库
    git clone https://github.com/modelcontextprotocol/servers.git
    cd servers/src/everything

    # 安装依赖并启动(监听 localhost:3001)
    npm install && node dist/sse.js

    这个 everything Server 是一个参考实现,提供了 add、echo 等示例工具,非常适合开发和测试。

    6.2 完整实现

    package dev.langchain4j.example.mcp;

    import dev.langchain4j.mcp.McpToolProvider;
    import dev.langchain4j.mcp.client.DefaultMcpClient;
    import dev.langchain4j.mcp.client.McpClient;
    import dev.langchain4j.mcp.client.transport.McpTransport;
    import dev.langchain4j.mcp.client.transport.http.HttpMcpTransport;
    import dev.langchain4j.model.chat.ChatModel;
    import dev.langchain4j.model.openai.OpenAiChatModel;
    import dev.langchain4j.service.AiServices;
    import dev.langchain4j.service.tool.ToolProvider;

    import java.time.Duration;
    import java.util.List;

    public class McpToolsExampleOverHttp {

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

    // ============ Step 1: 配置 ChatModel ============
    ChatModel model = OpenAiChatModel.builder()
    .apiKey("demo")
    .modelName("gpt-4o-mini")
    .baseUrl("http://langchain4j.dev/demo/openai/v1")
    .logRequests(true)
    .logResponses(true)
    .build();

    // ============ Step 2: 创建 HTTP 传输层 ============
    McpTransport transport = new HttpMcpTransport.Builder()
    .sseUrl("http://localhost:3001/sse") // MCP Server 的 SSE 端点
    .timeout(Duration.ofSeconds(60)) // 工具执行超时
    .logRequests(true) // 调试日志
    .logResponses(true)
    .build();

    // ============ Step 3-5: MCP Client → ToolProvider → AI Service ============
    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();

    // ============ Step 6: 对话 ============
    try {
    String response = bot.chat(
    "What is 5+12? Use the provided tool to answer " +
    "and always assume that the tool is correct."
    );
    System.out.println(response);
    } finally {
    mcpClient.close();
    }
    }
    }

    6.3 HTTP vs Stdio 的关键差异

    // Stdio 方式 —— 子进程通信
    McpTransport transport = new StdioMcpTransport.Builder()
    .command(List.of("npx", "-y", "server-package"))
    .environment(Map.of("KEY", "VALUE")) // 通过环境变量传递密钥
    .build();

    // HTTP 方式 —— 网络通信
    McpTransport transport = new HttpMcpTransport.Builder()
    .sseUrl("http://localhost:3001/sse") // 远程 SSE 端点
    .timeout(Duration.ofSeconds(60)) // 超时控制
    .build();

    HTTP 方式特有的考量:

    考量说明
    超时控制 工具执行可能很慢(如大数据查询),需要设置合理的超时
    认证 远程 Server 需要认证机制(API Key / OAuth / mTLS)
    断线重连 SSE 连接可能断开,需要考虑重连策略
    负载均衡 多个 Client 共享同一 Server,需要考虑并发能力
    TLS 生产环境建议使用 HTTPS

    七、MCP 安全模型深度解析

    MCP 协议从设计之初就将安全放在核心位置。下面我们深入分析 MCP 的安全模型。

    7.1 白名单机制(目录级沙箱)

    这是 @modelcontextprotocol/server-filesystem 的安全核心:

    MCP Server 启动命令:
    npx server-filesystem /allowed/path

    白名单目录:/allowed/path

    访问控制矩阵:

    请求路径 允许? 原因
    ─────────────────────────────────────────────────
    /allowed/path/file.txt ✅ 在白名单内
    /allowed/path/sub/deep/data.md ✅ 白名单的子目录
    /allowed/path/../other/file ❌ 试图跳出白名单
    /etc/passwd ❌ 绝对路径不在白名单
    ~/secret.key ❌ home 目录不在白名单
    ../../root/file ❌ 相对路径跳出

    实现原理(伪代码):

    // MCP Server 内部的文件访问校验逻辑
    String resolvePath(String requested) {
    Path allowed = Path.of("/allowed/path").toRealPath();
    Path target = allowed.resolve(requested).toRealPath();

    if (!target.startsWith(allowed)) {
    throw new SecurityException(
    "Access denied: " + requested + " is outside allowed directory"
    );
    }
    return target.toString();
    }

    7.2 环境变量隔离

    ┌─────────────────────────────────────────┐
    │ 父进程 (Java) │
    │ ┌─────────────────────────────────┐ │
    │ │ 环境变量: │ │
    │ │ PATH=/usr/bin │ │
    │ │ JAVA_HOME=/opt/jdk17 │ │
    │ │ DATABASE_PASSWORD=secret123 │ │ ← MCP Server 看不到!
    │ │ OPENAI_API_KEY=sk-xxx │ │ ← MCP Server 看不到!
    │ └─────────────────────────────────┘ │
    │ │ │
    │ │ ProcessBuilder │
    │ │ .environment(customEnv) │
    │ ▼ │
    │ ┌─────────────────────────────────┐ │
    │ │ 子进程 (MCP Server) │ │
    │ │ 环境变量: │ │
    │ │ BAIDU_MAP_API_KEY=xxx │ │ ← 仅此一个!
    │ │ (仅包含显式传递的变量) │ │
    │ └─────────────────────────────────┘ │
    └─────────────────────────────────────────┘

    这意味着:

    • MCP Server 子进程看不到父进程的环境变量(除非显式传递)
    • 你的数据库密码、API Key 等敏感信息不会泄露给 MCP Server
    • 每个 MCP Server 只能访问它需要的特定密钥

    7.3 工具级权限控制

    LangChain4j 的 McpToolProvider 不是"全有或全无"的。你可以通过 ToolProvider 的过滤机制实现工具级别的权限控制:

    // 获取 MCP Server 暴露的所有工具
    ToolProvider rawProvider = McpToolProvider.builder()
    .mcpClients(List.of(mcpClient))
    .build();

    // 只允许使用安全的工具
    ToolProvider filteredProvider = ToolProvider.builder()
    .tools(rawProvider.getTools().stream()
    .filter(tool -> isAllowed(tool.name()))
    .toList())
    .build();

    // 或者只允许特定前缀的工具
    private boolean isAllowed(String toolName) {
    return toolName.startsWith("read_") // 只允许读取,不允许写入
    || toolName.equals("echo"); // 测试工具
    }

    7.4 审计日志

    开启 logEvents(true) 可以完整记录 MCP 通信:

    [STDIO] → tools/list ← 获取工具列表
    [STDIO] ← 返回 3 个可用工具
    [STDIO] → tools/call read_file ← 调用读文件工具
    path=/allowed/file.txt
    [STDIO] ← 返回文件内容 200 字节
    [STDIO] → tools/call write_file ← 试图写文件
    [STDIO] ← 错误:write_file 不在工具列表中

    这些日志可用于:

    • 📊 安全审计:追踪 AI 调用了哪些工具、操作了什么资源
    • 🔍 问题排查:当 AI 行为异常时回溯工具调用链
    • 📈 使用分析:统计哪些工具最常被调用、哪些参数组合最常见

    7.5 安全最佳实践清单

    安全维度最佳实践
    白名单范围 给出最小必要的目录/资源访问范围
    API Key 管理 通过环境变量传递,不硬编码、不传命令行参数
    工具过滤 只暴露 AI 真正需要的工具(只读 vs 读写)
    超时限制 设置合理的工具执行超时,防止无限等待
    资源限制 限制 MCP Server 子进程的 CPU/内存使用
    日志审计 生产环境记录工具调用日志,定期审查
    版本锁定 使用精确版本号(如 @0.6.2),避免自动升级引入风险
    网络隔离 Stdio 模式天然网络隔离;HTTP 模式建议内网部署

    八、MCP 生态与社区

    8.1 官方 MCP Server 列表

    Anthropic 官方维护了一系列 MCP Server 参考实现:

    https://github.com/modelcontextprotocol/servers

    Server功能工具示例
    server-filesystem 文件系统操作 read_file, write_file, list_directory
    server-postgres PostgreSQL 数据库 execute_sql, list_tables, describe_table
    server-github GitHub API create_issue, search_code, get_pr
    server-brave-search 搜索引擎 web_search, local_search
    server-everything 测试/参考 add, echo, longRunningOperation
    server-slack Slack 集成 send_message, list_channels
    server-memory 持久化记忆 create_entities, search_nodes

    8.2 社区 MCP Server

    社区贡献了大量第三方 MCP Server,可在以下平台发现:

    • npm 搜索:@modelcontextprotocol/server-* 或 mcp-server-*
    • GitHub Topics:搜索 mcp-server 标签
    • MCP Hub:社区维护的 MCP Server 索引

    8.3 自己搭建 MCP Server

    如果你需要封装自己的工具为 MCP Server,LangChain4j 也支持构建 MCP Server 端。核心思路:

  • 使用 langchain4j-mcp 模块的 Server 端 API
  • 定义你的工具和资源
  • 选择 Stdio 或 HTTP 传输方式
  • 让任何 MCP Client(Java、Python、TypeScript…)消费你的工具
  • 这实现了真正的"一次编写,到处调用"。


    九、常见问题与避坑指南

    9.1 Windows 上 npx 无法执行

    错误现象:

    java.io.IOException: Cannot run program "npx": CreateProcess error=2

    原因:Windows 上 npx 是 .cmd 批处理文件,Java 无法直接执行。

    解决方案:

    if (isWindows) {
    command.add("cmd");
    command.add("/c");
    command.add("npx");
    } else {
    command.add("npx");
    }

    或者使用 npx 的绝对路径:

    command.add("D:/Program Files/nodejs/npx.cmd");

    9.2 工作目录不正确导致路径错误

    错误现象:

    MCP Server 报告:File not found: src/main/resources/file.txt

    原因:IDE 或 Maven 的运行工作目录可能不是项目根目录。

    解决方案:

    // 使用绝对路径
    String absolutePath = new File("src/main/resources")
    .getAbsolutePath();
    command.add(absolutePath);

    // 或显式设置工作目录
    ProcessBuilder pb = new ProcessBuilder(command);
    pb.directory(new File("/correct/working/dir"));

    9.3 模型不支持 Function Calling

    错误现象:AI 不调用工具,而是自行回答。

    原因:某些模型(如早期的文本生成模型)不支持 Function Calling。

    解决方案:

    • ✅ 使用 GPT-4o / GPT-4o-mini(最可靠)
    • ✅ 使用 DeepSeek-chat(支持 Function Calling)
    • ⚠️ 避免使用纯文本模型或超轻量模型

    验证方法:

    // 在提示词中明确引导模型使用工具
    String response = bot.chat(
    "What is 5+12? Use the provided tool to answer " +
    "and always assume that the tool is correct."
    );

    9.4 子进程成为僵尸进程

    错误现象:程序退出后,MCP Server 进程仍然在后台运行。

    原因:没有在 finally 块中调用 mcpClient.close()。

    解决方案:

    McpClient mcpClient = null;
    try {
    mcpClient = new DefaultMcpClient.Builder()
    .transport(transport)
    .build();
    // … 使用 mcpClient
    } finally {
    if (mcpClient != null) {
    mcpClient.close(); // 保证无论是否异常都清理子进程
    }
    }

    // 或在 Spring Boot 中将 McpClient 注册为 Bean 并设置 destroyMethod
    @Bean(destroyMethod = "close")
    public McpClient mcpClient() { ... }

    9.5 HTTP 传输的 SSE 连接超时

    错误现象:

    java.util.concurrent.TimeoutException: SSE connection timeout

    原因:工具执行时间过长,超过了客户端设置的超时时间。

    解决方案:

    // 增加超时时间
    .timeout(Duration.ofSeconds(120)) // 2 分钟

    // 或在 Server 端优化工具执行效率
    // 对于长时间操作,采用异步模式


    十、进阶技巧与最佳实践

    10.1 多 MCP Server 聚合

    当你的应用需要集成多个外部工具源时:

    // 文件系统 Server
    McpClient filesystemClient = new DefaultMcpClient.Builder()
    .transport(new StdioMcpTransport.Builder()
    .command(filesystemCommand)
    .build())
    .build();

    // 数据库 Server
    McpClient databaseClient = new DefaultMcpClient.Builder()
    .transport(new StdioMcpTransport.Builder()
    .command(databaseCommand)
    .build())
    .build();

    // 百度地图 Server
    McpClient baiduMapClient = new DefaultMcpClient.Builder()
    .transport(new StdioMcpTransport.Builder()
    .command(baiduMapCommand)
    .environment(Map.of("BAIDU_MAP_API_KEY", apiKey))
    .build())
    .build();

    // 聚合所有工具
    ToolProvider unifiedProvider = McpToolProvider.builder()
    .mcpClients(List.of(
    filesystemClient,
    databaseClient,
    baiduMapClient
    ))
    .build();

    // AI Service 现在拥有 3 个 MCP Server 的所有工具!
    Bot bot = AiServices.builder(Bot.class)
    .chatModel(model)
    .toolProvider(unifiedProvider)
    .build();

    10.2 MCP 与 @Tool 混合使用

    MCP 工具和本地 @Tool 可以并存:

    public class HybridAssistant {

    // 本地 @Tool:访问内部业务数据库
    @Tool("查询用户的订单列表")
    public List<Order> queryOrders(@P("用户ID") String userId) {
    return orderService.findByUserId(userId);
    }

    @Tool("查询用户信息")
    public User getUserInfo(@P("用户ID") String userId) {
    return userService.findById(userId);
    }
    }

    // 构建时同时注入本地工具和 MCP 工具
    HybridAssistant assistant = new HybridAssistant();
    Bot bot = AiServices.builder(Bot.class)
    .chatModel(model)
    .tools(assistant) // 本地 @Tool
    .toolProvider(mcpToolProvider) // MCP 远程工具
    .build();

    这种混合模式非常实用:

    • 📌 本地 @Tool:处理内部业务逻辑、数据库操作
    • ✅ MCP:接入外部服务、第三方 API、跨语言工具

    10.3 Spring Boot 环境下的 McpClient 生命周期管理

    @Configuration
    public class McpConfiguration {

    @Value("${npx.path}")
    private String npxPath;

    @Bean(destroyMethod = "close")
    public McpClient fileSystemMcpClient() {
    List<String> command = List.of(
    npxPath, "-y",
    "@modelcontextprotocol/server-filesystem@0.6.2",
    "/allowed/directory"
    );

    McpTransport transport = new StdioMcpTransport.Builder()
    .command(command)
    .build();

    return new DefaultMcpClient.Builder()
    .transport(transport)
    .build();
    }

    @Bean
    public ToolProvider mcpToolProvider(List<McpClient> mcpClients) {
    return McpToolProvider.builder()
    .mcpClients(mcpClients)
    .build();
    }
    }

    利用 Spring 的 @Bean(destroyMethod = "close") 确保应用关闭时自动清理 MCP 子进程。

    10.4 错误处理与降级

    public String chatWithFallback(String prompt) {
    try {
    return bot.chat(prompt); // 尝试使用 MCP 工具
    } catch (Exception e) {
    logger.warn("MCP 工具调用失败,降级到纯文本回答", e);

    // 降级:不带工具的纯对话
    Bot botWithoutTools = AiServices.builder(Bot.class)
    .chatModel(model)
    .build();
    return botWithoutTools.chat(
    prompt + "\\n(注意:工具服务暂时不可用,请基于你的知识回答)"
    );
    }
    }


    十一、总结与展望

    本文核心要点回顾

    知识点核心内容
    MCP 定位 标准化 LLM 与外部工具/数据源的通信协议
    两大传输 Stdio(子进程通信,适合本地工具)vs HTTP/SSE(网络通信,适合远程服务)
    核心 API StdioMcpTransport / HttpMcpTransport → DefaultMcpClient → McpToolProvider
    安全设计 白名单目录、环境变量隔离、工具级权限过滤、审计日志
    MCP vs @Tool 跨语言复用 → MCP;简单本地工具 → @Tool
    生态现状 官方 7+ Server + 社区大量第三方 Server

    适用场景速查

    你的需求 推荐方案
    ─────────────────────────────────────────────────────
    集成已有的 Python/TS 工具服务 → MCP
    使用社区现成的 MCP Server(地图、搜索等) → MCP
    多个 Java 应用共享同一组工具 → MCP
    简单的本地 Java 方法暴露给 AI → @Tool
    仅在本应用内使用的业务逻辑 → @Tool
    需要跨语言、跨平台复用 → MCP


    延伸阅读

    • MCP 协议官方规范
    • MCP 官方 GitHub 仓库
    • MCP Server 官方列表
    • LangChain4j MCP 模块文档
    • Anthropic 官方 MCP 介绍文章

    在这里插入图片描述

    🎯 更多专栏系列文章:LangChain4j Java AI应用开发实战、🔥 其他专栏可以查看博客主页

    🔔 关于作者:资深程序老猿,10年+架构经验,现专注 AIGC 探索与实践。 👍 若文章对你有所触动,恳请点赞 ⭐ 关注 ⭐ 收藏!AI 浪潮已至,愿与你同行。

    赞(0)
    未经允许不得转载:171主机测评 » LangChain4j Java AI 应用开发实战(二十八):MCP 协议 —— 安全的工具与数据源接入标准
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址