MCP 的核心作用
如果说大模型(LLM)是操作系统的大脑,那么 MCP(Model Context Protocol,模型上下文协议)就是大模型的 “USB 接口”。在 MCP 出现之前,大模型要连接外部世界(查数据库、调企业 API、看本地文件),每个开发者都要自己用 Python/Java 写一套定制的 Tools(Tools Calling)。这导致了严重的碎片化:你给 OpenAI 写的工具,换到 Claude 或本地的 Ollama 上,可能就要重写适配。
Anthropic 推出的 MCP 彻底改变了这一点。它统一了标准:它建立了一个轻量级、双向的开放协议,让任何大模型(Client)都可以通过统一的标准,秒级接入任何数据源和工具生态(Server)。MCP 的作用可以概括为以下三点:
- Resources(资源):把静态数据(如:日志文件、DB 某一页、ReadMe)以统一格式读给大模型。
- Prompts(提示词模板):Server 侧提供预设的 Prompt 模板(如:“帮我 review 这段 Java 代码”)。
- Tools(工具):让大模型可以 “做动作”(如:在百度地图查经纬度、去 GitHub 提个 PR、在 Redis 里删个 Key)。
关于 MCP 的介绍,也可以参考本站之前写的《Langchain4j - MCP 和 Tool Calling 的对比及其具体案例》。
MCP 的正确玩法
在 Demo 阶段,大家喜欢用 stdio(标准输入输出)命令行直接拉起一个本地进程(例如 npx baidu-map),简单粗暴。但如果把这套东西直接扔上生产环境,基本等同于灾难。生产环境需要围绕安全、高可用、隔离和工程化展开。
- 第一,全面抛弃 stdio,全量拥抱 sse(网关化),绝对不推荐让微服务进程去 fork 本地子进程。在 LLM 和一堆 MCP Server 之间架设一个 MCP 网关(或 API Gateway)。大模型只需要连接这个网关,由网关负责路由、鉴权、限流(Rate Limiting)。比如把百度的、GitHub的、内部基础设施的 MCP Server 全挡在网关后面。
- 第二,动态工具注入(Dynamic Tool Injection)与权限割裂。大模型并不是越聪明越好,如果每次请求都把几十个工具(比如查地图、删数据库、发邮件)一股脑塞给它,不仅会触发 Token 爆量(如你第一问遇到的 400 错误或上下文浪费),还会导致大模型 “工具选择综合征”(选错工具)。正确的做法是基于用户身份和当前上下文,动态注入工具。普通用户进来了,只注入 query-product-mcp(只读工具);管理员进来了,才动态注入 delete-user-mcp(写工具)。
- 第三,独裁的 “沙箱化” 与 “降权执行”。生产环境的 MCP Server 往往拥有很高的权限(比如能执行 Shell 命令、查核心 DB)。大模型一旦遭遇提示词注入攻击(Prompt Injection),可能会被攻击者教唆去执行 rm -rf 或拖库。所以:
- 隔离:MCP Server 必须部署在极度严格的私有网络或 K8s 沙箱环境中,只能单向访问特定资源。
- 降权:运行 node 或 python MCP 进程的 Linux 用户绝对不能是 root,必须是无特殊权限的临时用户(如 nobody)。
- 第四,双表单确认机制。涉及到敏感操作的 Tools(如:线上退款、发送邮件给大客户、修改配置),绝对不能让 LLM 自动“一条龙”执行完。MCP Server 在收到敏感动作请求时,不直接执行,而是先在数据库生成一个“待审批任务”,并返回给大模型一个 need_approval_id。大模型向前端输出:“我已经为您准备好了退款申请,请点击下方链接确认。” 只有当真正的人类点击了按钮,Tool 才会真正生效。
- 第五,分布式追踪与审计。当大模型开始频繁调用 MCP 时,排查线上 Bug 会变成地狱:用户提问 -> LLM 思考 -> 调 MCP A -> 调 MCP B -> LLM 总结 -> 返回。中间任何一个环节慢了或者报错了,怎么查?必须利用 Spring Cloud Sleuth / OpenTelemetry,将 TraceId 强行透传进 MCP 的 SSE 请求头中。在 SkyWalking 或 ELK 上,你可以清晰地看到一条调用链:大模型到底卡在自己思考上,还是卡在百度地图的 API 响应上。
总结起来,实际企业落地的生产架构通常长这样:
1 2 3 4 5 6 7 8 9 10
| [ 用户 / 前端 ] │ ▼ [ 核心微服务 (ChatClient) ] ──(鉴权获取 Token)──► [ 统一认证中心 ] │ ▼ (通过 SSE 路由) [ MCP Gateway 网关 ] ├──► [ 百度地图 MCP Server ] (Pod A) ──► 外部 API ├──► [ 内部权限 DB MCP Server ] (Pod B) ──► 只读从库 └──► [ 自动化运维 MCP Server ] (Pod C) ──► 严格隔离的沙箱环境
|
总之,就是把大模型当成一个随时可能被黑客控制的、极不稳定的 “实习生”。所有的 MCP Server 就是实习生能调用的内部系统,你必须用网关限流、沙箱隔离、人类审批、链路追踪这些套路,把它死死地锁住。
写一个 MCP 服务端
这里的代码只做演示之用。
首先是依赖配置:
1 2 3 4 5 6 7 8 9 10
|
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server-webflux</artifactId> </dependency>
|
配置文件:application.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| server: port: 8090 servlet: encoding: charset: UTF-8 enabled: true force: true
spring: ai: mcp: server: type: async name: owlias-mcp-server version: 1.0.0
|
Tools 的实现(这里举例说明):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import lombok.extern.slf4j.Slf4j; import org.springframework.ai.tool.annotation.Tool;
@Slf4j public class WeatherTool { public record WeatherRequest(String location) {} public record WeatherResponse(String location, String temperature, String info, String wind) {}
@Tool(description = "根据城市名称或地点获取当前的实时天气预报") public WeatherResponse chatWeather(WeatherRequest request) { log.info("请求获取天气信息:{}", request); if (request.location().contains("北京")) { return new WeatherResponse("北京", "1℃", "晴朗", "北风3级"); } else if (request.location().contains("上海")) { return new WeatherResponse("上海", "2℃", "大雨", "东风2级"); } else { return new WeatherResponse(request.location(), "20℃", "阴天", "微风"); } } }
|
将 Tools 列表暴露出去:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import demo05.tools.WeatherTool; import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.ai.tool.method.MethodToolCallbackProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;
@Configuration public class McpServerConfig {
@Bean public ToolCallbackProvider weatherTools() { return MethodToolCallbackProvider.builder() .toolObjects(new WeatherTool()) .build(); } }
|
客户端使用 MCP
引入依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter-dashscope</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-client</artifactId> </dependency>
|
配置文件:application.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| server: port: 8080 servlet: encoding: charset: UTF-8 enabled: true force: true
spring: ai: dashscope: api-key: ${QWEN_API_KEY} openai: api-key: api-key-xxx base-url: http://localhost:11434 chat: options: model: gemma3:1b mcp: client: type: async request-timeout: 60s toolcallback: enabled: true sse: connections: mcp-server1: url: http://localhost:8090
logging: level: org.springframework.ai.chat.client.advisor: DEBUG
|
配置类:SsaLLMConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.web.client.RestClientCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.SimpleClientHttpRequestFactory; import java.time.Duration;
@Configuration public class SsaLLMConfig {
private static final String apiKey; static { apiKey = System.getenv("QWEN_API_KEY"); }
@Bean public RestClientCustomizer restClientCustomizer() { return restClientBuilder -> { SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setReadTimeout(Duration.ofSeconds(300)); factory.setConnectTimeout(Duration.ofSeconds(100)); restClientBuilder.requestFactory(factory); }; }
@Bean("dashScopeApi") public DashScopeApi dashScopeApi() { return DashScopeApi.builder().apiKey(apiKey).build(); }
@Bean("deepseekChatModel") public ChatModel deepseekChatModel(DashScopeApi dashScopeApi) { return DashScopeChatModel.builder() .dashScopeApi(dashScopeApi) .defaultOptions(DashScopeChatOptions.builder().model("deepseek-v4-flash").build()) .build(); }
@Bean("deepseekChatClient") public ChatClient deepseekChatClient(@Qualifier("deepseekChatModel") ChatModel deepseekChatModel, ToolCallbackProvider tools) { return ChatClient.builder(deepseekChatModel) .defaultToolCallbacks(tools.getToolCallbacks()) .defaultAdvisors(new SimpleLoggerAdvisor()) .build(); } }
|
业务测试类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @RestController @RequestMapping("/mcp-test") public class McpClientTestController {
@Resource(name = "deepseekChatClient") private ChatClient chatClient;
@Resource(name = "deepseekChatModel") private ChatModel chatModel;
@GetMapping(value = "/test01", produces = MediaType.TEXT_PLAIN_VALUE) public Flux<String> test01(@RequestParam(defaultValue = "北京天气") String message) { return chatClient.prompt() .user(message) .stream() .content(); }
@GetMapping(value = "/test02", produces = MediaType.TEXT_PLAIN_VALUE) public Flux<String> test02(@RequestParam(defaultValue = "北京天气") String message) { return chatModel.stream(message); } }
|
调用第三方的 MCP 服务
这里以百度地图为例说明。参考:MCP.so 百度地图 MCP Server。百度地图已经全面拥抱 MCP 协议,只是它默认采用了本地 stdio fork 子进程的方式,不推荐使用这种方式部署到生产,这里仅作为演示操作。
使用百度地图,让我们本地大模型具有查位置查天气等功能,我们只需要在上述 客户端使用 MCP 的基础上加一个配置即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| spring: ai: mcp: client: type: async request-timeout: 60s toolcallback: enabled: true sse: connections: mcp-server1: url: http://localhost:8090 stdio: servers-configuration: classpath:/mcp-server.json5
|
resources/mcp-server.json5
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| { "mcpServers": { "baidu-map": { "command": "npx", "args": [ "-y", "@baidumap/mcp-server-baidu-map" ], "env": { "BAIDU_MAP_API_KEY": "xxx" } } } }
|
标题:
Spring AI - MCP 用法和基本测试案例