上一话的 @Tool 有个硬伤:工具与 Agent 必须在同一个 JVM 里。
MCP 就是把这层耦合拆掉的开放协议 —— 它让"能力"变成可独立部署、可被发现、可跨语言复用的服务。
没有 MCP 时的世界:N 个 AI 应用 × M 个数据源 = N×M 套适配器。有了 MCP:N + M。
进阶:Sampling(服务端反向请求模型补全)、Roots、Elicitation(向用户追问)。
| 传输方式 | 怎么连 | 适用 | 代价 |
|---|---|---|---|
| stdio | Client 启动一个本地子进程,用 stdin/stdout 收发 JSON-RPC 消息 | 本地工具、命令行工具、桌面客户端(最广泛) | 进程生命周期管理;日志不能打 stdout;难水平扩展 |
| SSE / HTTP | HTTP POST 发请求 + SSE 推流 | 远程共享服务、团队级能力中台 | 需要鉴权、TLS、网关;断线重连 |
| 同进程直连 | 不跨网络(就是第 06 话的 @Tool) | 高性能、强事务、需要共享 Spring 上下文 | 无法跨语言/跨团队复用 |
// pom: spring-ai-starter-mcp-server-webmvc(或 -webflux)
@Service
public class K8sTools {
@Tool(description = "查询指定命名空间下处于非 Running 状态的 Pod 列表,返回 Pod 名称、状态与重启次数") //!hl
public List<PodInfo> listAbnormalPods(
@ToolParam(description = "Kubernetes 命名空间,如 prod、staging") String namespace) {
return k8sService.listAbnormal(namespace);
}
@Tool(description = "获取某个 Pod 最近 N 行日志,用于排障") //!hl
public String podLogs(
@ToolParam(description = "Pod 名称") String pod,
@ToolParam(description = "命名空间") String namespace,
@ToolParam(description = "日志行数,默认 200", required = false) Integer lines) {
return k8sService.tailLogs(namespace, pod, lines == null ? 200 : lines);
}
}
@Configuration
class McpServerConfig {
@Bean
ToolCallbackProvider k8sToolProvider(K8sTools k8sTools) { //!hl
return MethodToolCallbackProvider.builder()
.toolObjects(k8sTools)
.build();
}
}
# application.yml(Server 端)
spring:
ai:
mcp:
server:
name: ops-mcp-server //!hl
version: 1.0.0
type: SYNC # SYNC 或 ASYNC
# stdio 模式(本地被拉起时用)
stdio: true
# 若用 HTTP/SSE 模式:
# sse-message-endpoint: /mcp/message
# capabilities:
# tool: true
# resource: true
# prompt: true
stdio 模式下,你的应用绝对不能往 stdout 打印任何东西。
Spring Boot 默认日志输出到控制台(stdout),会直接把 JSON-RPC 报文污染成「半行日志 + 半行 JSON」,
表现为连接随机断开、工具调用莫名失败。
解决:stdio 模式启动时把日志重定向到 stderr 或文件(logging.pattern.console= + 文件 appender,或启动脚本里 2>&1 之外单独处理)。
这个坑能让人排查一整天。
# application.yml(Client 端)
spring:
ai:
mcp:
client:
stdio:
connections:
ops-server: //!hl
command: java //!hl
args: ["-jar", "/opt/ops-mcp-server.jar"] //!hl
env:
KUBECONFIG: /etc/kube/config
sse:
connections:
jira-server:
url: https://mcp.internal.example.com/jira
# 需要鉴权时走 header / 网关
@Bean
ChatClient opsAgent(ChatClient.Builder builder,
ToolCallbackProvider mcpTools) { //!hl 由 MCP Starter 自动装配进来
return builder
.defaultSystem("你是运维排障助手,先取证再下结论,禁止臆测。")
.defaultToolCallbacks(mcpTools) //!hl
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory()).build())
.build();
}
// 也支持外部配置文件,与 Claude Desktop 同款格式:
// spring.ai.mcp.client.stdio.servers-configuration=classpath:/mcp-servers-config.json
ToolCallback,通过 ToolCallbackProvider 暴露给 ChatClient。从这一步起,与本地 @Tool 完全同构,模型根本不知道工具在哪个进程。McpSyncClient / McpAsyncClient 发 JSON-RPC → 拿回结果 → 转成 ToolResponseMessage 回填。注意第 3 步:MCP 工具与本地工具在 ChatClient 眼里是同一种东西。这意味着你可以自由混用 —— 本地 @Tool 做高频、强事务的操作,MCP 做跨系统、跨语言的能力。
| 维度 | 本地 @Tool | MCP Server |
|---|---|---|
| 部署耦合 | 同 JVM,改工具要重启 Agent | 独立进程,可独立发布与扩缩容 |
| 语言 | 只能 Java | 任意语言(Python/Go/TS 都能写 Server) |
| 复用范围 | 本应用 | 整个公司 / 整个生态(含第三方客户端) |
| 性能 | 方法调用,微秒级 | 进程/网络开销,毫秒到百毫秒级 |
| 事务与上下文 | 可直接参与 Spring 事务、共享 SecurityContext | 跨进程,需自行传递身份与幂等键 |
| 可观测 | 天然进入现有 APM | 需额外埋点(跨进程 traceId 传递) |
| 动态发现 | 不支持(编译期确定) | 支持(tools/list + 变更通知) |
同一个应用内、强事务、高频调用 → 用 @Tool。
要被多个 Agent 复用、要跨语言、要独立发布、或本来就是别人的系统 → 用 MCP。
不要为了"上协议"而上协议 —— 一次本地方法调用被拆成跨进程 RPC,是把简单问题复杂化。
stdio 的 command / args 绝不能拼接用户输入。否则用户一句恶意提问就能让你的服务器执行任意命令。
command 必须是配置里的常量。
MCP Server 往往以"服务账号"运行,权限远大于请求它的用户。必须在 Server 侧再做一次用户级鉴权, 把调用者身份随请求透传,不能只看"来自内网"。
工具返回的内容(网页、日志、Jira 描述)可能包含提示词注入。把外部内容当不可信输入,必要时做净化与隔离提示。
连上 5 个 Server 就是 80 个工具 → token 暴涨 + 选择准确率暴跌。按场景裁剪工具集,只挂载当前任务需要的 Server。
npx/uvx 的可执行文件解析方式与 Linux 不同,容易出现"命令找不到"。优先用绝对路径 + 明确 .cmd。第 09 话 —— 现在你的 Agent 有脑、有手、有记忆、有知识、有生态。下一话解决最后一块拼图:一个 Agent 干不完的活,怎么拆给一群 Agent —— 五种编排模式的 Spring 落地写法。