MCP (Model Context Protocol) 是什么?面向 Java 开发者的解释
1. 总览
MCP (Model Context Protocol) 是一个开放标准,帮助 LLM (large language model) 以统一的方式连接到数据、工具和外部系统,而不是每个 AI 应用都必须为每个数据源单独编写集成代码。本文解释了 MCP 是什么、其架构如何,并指导你使用 Spring Boot(通过 Spring AI)编写一个简单的 MCP server,以向 LLM 公开一个工具。读完本文后,你将了解何时应该使用 MCP,何时它只是过度设计。
2. MCP 解决的问题
在 MCP 出现之前,每个 AI 应用(聊天机器人、agent)想要访问数据库、文件系统、内部 API……都必须编写单独的适配器。如果你有 M 个 AI 应用和 N 个数据源,你就需要 M × N 种不同的集成代码。
MCP 标准化了这层通信:AI 应用(称为 Host)只需按 MCP 标准说一次,任何暴露 MCP server 的系统都能与其“对话”。就像电子设备的 USB-C 一样——一个统一的标准替代了数十种不同的接口。
注意: MCP 不是替代 REST 或 gRPC 的框架,用于常见的 service-to-service 通信。它的用途是让 LLM/agent 调用工具,而不是让两个微服务后端相互通信。
3. MCP 架构
MCP 有 3 个主要角色:
- Host:包含 LLM 的应用(例如 Claude Desktop、你自己编写的 agent)。
- Client:Host 内部的组件,与一个 MCP server 保持 1 对 1 的连接。
- Server:暴露 capability(工具、资源、提示词)的进程,遵循 MCP 标准。
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Host │ 1-1 │ MCP Client │ JSON-RPC │ MCP Server │
│ (chứa LLM) │◄──────►│ │◄──────────►│ (DB, API, file) │
└──────────────┘ └──────────────┘ └──────────────────┘
Client 和 server 之间的通信使用 JSON-RPC 2.0,通过两种常见的传输方式之一传输:
- stdio:server 作为子进程运行,client/server 通过 stdin/stdout 通信。适合于 server 在本地运行的情况。
- SSE/HTTP (Streamable HTTP):用于当 server 在远程运行、通过网络通信时。
3.1. 三种 capability
| Capability | 含义 | 示例 |
|---|---|---|
| Tools | LLM 可以调用来执行操作的函数 | getWeather(city)、createOrder(...) |
| Resources | LLM 可以读取的数据(类似 GET) | 文件日志、数据库记录 |
| Prompts | 由 server 预定义的 prompt 模板 | “按模板 X 总结订单” |
大多数实际用例都围绕 Tools,所以下面的示例将重点关注这一点。
4. 使用 Spring AI 编写 MCP server
Spring AI(从 1.0.0-M6 版本开始)通过专用的 starter 支持 MCP,允许你通过注解将 Java bean 暴露为 MCP tool,无需自己实现 JSON-RPC。
4.1. 添加依赖
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>
4.2. 定义工具
@Service
public class WeatherService {
@Tool(description = "获取城市的当前温度")
public String getTemperature(String city) {
return switch (city.toLowerCase()) {
case "ha noi" -> "28°C,多云";
case "sai gon" -> "32°C,晴天";
default -> "没有该城市的数据:" + city;
};
}
}
@Tool将这个方法标记为一个 MCP tool。description 属性非常重要——LLM 使用这个描述来决定是否应该调用该工具以及用什么参数调用。getTemperature接受一个String city参数,返回描述天气的字符串。MCP server 会自动为参数和返回类型生成 JSON schema,以供 LLM 理解。
4.3. 向 MCP server 注册工具
@Configuration
public class McpServerConfig {
@Bean
public ToolCallbackProvider weatherTools(WeatherService weatherService) {
return MethodToolCallbackProvider.builder()
.toolObjects(weatherService)
.build();
}
}
MethodToolCallbackProvider 扫描 weatherService 中带有 @Tool 的方法,并在应用上下文启动时自动将它们注册到 MCP server。
4.4. 配置服务器
spring.ai.mcp.server.name=weather-mcp-server
spring.ai.mcp.server.version=1.0.0
spring.ai.mcp.server.stdio=true
设置 stdio=true 时,server 作为独立进程运行,通过 stdin/stdout 通信——适合快速测试 Claude Desktop 或自己编写的 MCP client。如果想将 server 部署为远程独立服务,可以改用 SSE 传输并通过 HTTP endpoint 公开,而不是 stdio。
启动服务器:
mvn spring-boot:run
Started WeatherMcpApplication in 1.124 seconds
Registered MCP tool: getTemperature
5. 从客户端调用 MCP server
假设你编写了一个需要调用上述 getTemperature tool 的 Host 应用(例如内部 agent)。
5.1. 客户端依赖
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>
5.2. 调用工具
@Component
public class WeatherAgent {
private final McpSyncClient mcpClient;
public WeatherAgent(McpSyncClient mcpClient) {
this.mcpClient = mcpClient;
}
public String askTemperature(String city) {
CallToolRequest request = new CallToolRequest(
"getTemperature",
Map.of("city", city));
CallToolResult result = mcpClient.callTool(request);
return result.content().get(0).text();
}
}
McpSyncClient是 Spring AI 根据application.yml中声明的传输配置(stdio 或 SSE)自动创建的 client bean。CallToolRequest将工具名称(getTemperature)和参数(city)打包成一个 JSON-RPC 调用。result.content()返回响应内容列表;这里取第一个text部分。
客户端配置(例如通过将 server jar 作为子进程运行来通过 stdio 连接到 server):
spring:
ai:
mcp:
client:
stdio:
connections:
weather-server:
command: java
args: -jar weather-mcp-server.jar
6. 使用 JUnit 5 + AssertJ 测试工具
由于 @Tool 只是 Java 普通方法上的注解,你可以独立于 MCP 测试业务逻辑——无需启动整个 server:
class WeatherServiceTest {
private final WeatherService weatherService = new WeatherService();
@Test
void shouldReturnTemperatureForKnownCity() {
String result = weatherService.getTemperature("Ha Noi");
assertThat(result).contains("28°C");
}
@Test
void shouldReturnFallbackMessageForUnknownCity() {
String result = weatherService.getTemperature("Tokyo");
assertThat(result)
.startsWith("没有该城市的数据:")
.contains("Tokyo");
}
}
如果想测试 MCP 集成部分(tool 是否以正确的名称和 schema 注册),可以在 @SpringBootTest 级别编写测试并检查 ToolCallbackProvider:
@SpringBootTest
class McpServerConfigTest {
@Autowired
private ToolCallbackProvider toolCallbackProvider;
@Test
void shouldExposeTemperatureTool() {
var toolNames = toolCallbackProvider.getToolCallbacks().stream()
.map(callback -> callback.getToolDefinition().name())
.toList();
assertThat(toolNames).contains("getTemperature");
}
}
这个测试确保每次重构 WeatherService 时,tool 仍然以正确的名称公开——避免出现更改方法名称但忘记更新 LLM 引用该方法的地方的情况。
7. 常见错误和何时不应使用 MCP
工具描述太模糊。 如果 description 只是写“获取数据”,当有多个类似工具时,LLM 很容易选错工具。应该写具体的 description,明确说明预期的输入/输出。
使用 MCP 替代内部 REST/gRPC。 MCP 添加了一个 JSON-RPC 和传输层,针对 LLM 动态调用工具的场景进行了优化——对于两个后端服务高频率、低延迟的调用不优化。在这里使用 MCP 是过度设计。
未控制工具的权限。 MCP server 通常以托管它的 process 的权限运行。如果你暴露的工具可以删除文件或运行 shell 命令而不限制输入参数,LLM(或 prompt injection)可能会滥用该工具。始终验证工具中的参数,不要盲目信任来自 LLM 的输入。
在无服务器环境中运行 stdio server。 stdio 传输需要生成子进程——在无服务器/短生命周期容器环境中效果不好。这种情况下应该使用 SSE/HTTP 传输。
client 和 server 版本不同步。 MCP 仍在快速发展中(spec 版本之间有许多 breaking change)。升级 SDK 时,部署到生产环境前要仔细检查更改日志。
8. 结论
MCP 标准化了 LLM 调用工具和读取外部数据的方式,解决了 AI 应用和数据源之间的 M × N 集成问题。使用 Spring AI,你只需通过 @Tool 注解就可以将 Java 服务暴露为 MCP tool,无需自己实现 JSON-RPC。当你构建需要根据 LLM 的决策动态调用工具的 agent/chatbot 时,应该使用 MCP——避免在常见的 service-to-service 通信中使用它,那些场合 REST 或 gRPC 仍然是更合适的选择。