MCP 实战手记系列(二)· 实操入门
系列(一)把 MCP 的现状和几个拐点铺开了,但光看不练没体感。这篇动手把第一个 MCP Server 跑起来,能启动、能调用、有真实返回。
面向一线开发者,重实操,每篇都有我亲手跑过的东西。
引子:先别管协议细节,把灯点亮
系列(一)里我说过一句大白话:MCP 干的事,就是把"工具的调用"标准化,让大模型能直接使唤你写的函数。
但落到代码上,很多人第一步就卡住,觉得得先啃完那份几百页的协议规范。不用。Spring AI 把 MCP Server 封装到了加个注解就能用的程度,你完全可以先跑起来,再回头看协议。
这篇只做一件事:用 Spring AI 把第一个 MCP Server 跑通,让它真的能回答"机房现在多少度""某台设备开着没"。代码都在仓库里,全 mock 数据,clone 下来就能跑。
一、这次要跑什么
场景还是系列(三)用过的机房环境监控,不过这次是从零搭的版本:
get_room_environment:返回机房温度、湿度get_device_status:传入设备名(如ac-01),返回它的运行状态
数据全是内存 mock,不连数据库、不连真实设备。这么设计是想让你把精力放在"MCP Server 怎么搭"上,而不是被业务依赖绊住。
二、最小骨架:三样东西
一个能用的 MCP Server,骨架就三样。少一样跑不起来,多一样是冗余。
① 依赖
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
<version>1.0.0-M7</version>
</dependency>
注意 1.0.0-M7 是里程碑版本,不是正式版。里程碑版要额外加 Spring 的里程碑仓库,而且别上生产,它只是用来演示旧写法的。正式版是 2.x,系列(三)会用到。
② 配置(application.yml)
yaml
server:
port: 8080
spring:
ai:
mcp:
server:
name: room-monitor-mcp-server
sse-message-endpoint: /mcp/message
sse-message-endpoint 是旧规范(SSE 传输)的标志性配置。先记住它,系列(三)改无状态时这行会被删掉。
③ 工具
普通 @Service 里的方法,加 @Tool / @ToolParam 注解,就变成 MCP 工具,不用实现任何接口:
java
@Tool(name = "get_room_environment", description = "获取机房环境数据(温度、湿度)")
public RoomEnvironment getRoomEnvironment() {
return new RoomEnvironment(23.5, 48.2); // 真实项目里查数据库 / 时序库
}
再把服务注册成工具源:
java
@Bean
public ToolCallbackProvider roomMonitorToolCallbackProvider(RoomMonitorService svc) {
return MethodToolCallbackProvider.builder().toolObjects(svc).build();
}
这套声明式写法是 Spring AI 我最喜欢的地方。协议演进不该动你的业务代码,系列(三)会验证这一点,迁移时业务方法几乎一行不用改。
三、跑起来
cd 02-first-server && mvn spring-boot:run(端口 8080)。启动日志里这几行是重点:
text
c.ethanliang.mcp.config.McpToolConfig : Registered Tool: get_device_status
c.ethanliang.mcp.config.McpToolConfig : Registered Tool: get_room_environment
o.s.a.m.s.a.McpServerAutoConfiguration : Registered tools: 2, notification: true
o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
c.ethanliang.mcp.FirstServerApplication : Started FirstServerApplication ...
两个工具都注册上了,服务在 8080 监听,灯就亮了。
顺带说个你可能会担心的点:这个项目
java.version设的是 17,但我本地是 JDK 21 直接跑通的。17 只是编译目标字节码版本,JDK 21 既能编译也能运行(Spring Boot 3.3 要求 Java 17+,21 满足)。所以你不用为了它去装 JDK 17。
四、怎么调它(不装任何客户端)
MCP 走 JSON-RPC。SSE 传输下,不能直接 POST 工具调用,得先握手:
GET /sse打开事件流,服务端回一个endpoint事件,里面带sessionId的 POST 地址(类似/mcp/message?sessionId=xxx)- 拿到地址后,再
POST你的 JSON-RPC 消息过去 - 响应从刚才那条 SSE 流里回来
我写了个几十行的 Python 脚本(纯标准库)走完这套流程,下面都是实跑返回,没改过。
先 initialize 握手,服务端亮明身份:
json
{
"protocolVersion": "2024-11-05",
"capabilities": { "logging": {}, "tools": { "listChanged": true } },
"serverInfo": { "name": "room-monitor-mcp-server", "version": "1.0.0" }
}
tools/list 看有哪些工具(中文描述是我在代码里写的 @Tool 说明):
json
{
"tools": [
{
"name": "get_device_status",
"description": "获取指定设备的运行状态",
"inputSchema": {
"type": "object",
"properties": {
"deviceName": { "type": "string", "description": "设备名称,如 ac-01、fan-02、light-03" }
},
"required": ["deviceName"],
"additionalProperties": false
}
},
{
"name": "get_room_environment",
"description": "获取机房环境数据(温度、湿度)",
"inputSchema": { "type": "object", "properties": {}, "required": [], "additionalProperties": false }
}
]
}
真正调用,get_room_environment 返回:
json
{ "content": [ { "type": "text", "text": "{\"temperature\":23.5,\"humidity\":48.2}" } ], "isError": false }
get_device_status 传 ac-01,返回:
json
{ "content": [ { "type": "text", "text": "\"ac-01 运行中\"" } ], "isError": false }
到这步,一个 MCP Server 从代码到调用就完整闭环了。大模型侧只要是个支持 MCP 的客户端,把这套端点接上去就能用这两个工具。
五、踩坑记录
实跑下来几个坑,按搜索价值排:
① 编码坑:中文 Windows 上 JVM 默认是 GBK
我本地是中文 Windows,JDK 文件编码默认 GBK。一开始工具描述里的中文,在客户端按 UTF-8 解析出来是一堆黑块和问号,原因是 MCP 把中文按 GBK 序列化了。
解法 :启动加 -Dfile.encoding=UTF-8:
bash
java -Dfile.encoding=UTF-8 -jar target/first-server-1.0.0-SNAPSHOT.jar
如果你本地本来就是 UTF-8 环境(多数 Linux / macOS,或设了环境变量的 Windows),不会撞这个坑,可以忽略。
② 握手坑:不能直接 POST /mcp/message
SSE 传输下,/mcp/message 是要带 sessionId 的,而 sessionId 得先 GET /sse 拿。直接 POST 会拿到 Invalid message format (-32600) 之类的错,不是协议问题,是顺序错了。先开流、再拿地址、最后发消息,三步不能省。
③ 依赖坑:里程碑版要配仓库,且别上生产
1.0.0-M7 不在 Maven 中央仓的正式路径里,pom 得加 spring-milestones 仓库。另外它是里程碑版,API 可能变、不建议生产用。系列(三)会切到正式版 2.x,正好对照。
六、这个服务器,下一篇就拆它
现在跑起来的这个,是有状态的。客户端建 SSE 长连接,服务端分配 session 存状态。水平扩展时,session 不共享就会出事。
系列(三)《把 MCP Server 改成无状态》做的一件事,就是把这样一个 SSE 有状态服务器,改成无状态 Streamable HTTP。你会看到改动 90% 集中在配置和依赖,业务工具代码基本不动。这篇文章的 02-first-server,就是那篇的"改造前"。
小结
- 跑通比想象简单 :依赖 + 配置 +
@Tool,三样凑齐就能用 - 声明式工具定义是甜点:业务方法加个注解变成 MCP 工具,协议演进不绑架你的代码
- 它现在是有状态的:下篇拆掉 session,看改动到底落在哪
仓库已经就位,git checkout v02 就能把这一篇的代码原样跑起来。先把这个最小骨架吃熟,后面几篇的改造才有落脚点。
本文完整可运行代码: github.com/ethanliang2016/mcp-in-action,对应目录
02-first-server,标签v02。git checkout v02即可还原这一篇的状态。
MCP 实战手记系列路线图
| # | 篇目 | 状态 |
|---|---|---|
| 1 | 总纲篇:MCP 到哪一步了 | ✅ |
| 2 | 跑通第一个 MCP Server(本篇) | ✅ |
| 3 | 把 MCP Server 改成无状态 | ✅ |
| 4-7 | CIMD 授权 / MCP Apps / 自建网关 / 安全篇 | 规划 |
后面几篇逐步放出,关注我,更新第一时间能看到。
你跑第一个 MCP Server 时卡在哪一步?评论区说,有价值的我整理进后续篇目。
参考: