MCP实战手记系列(二):跑通第一个 MCP Server(文末附github源码链接)

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 工具调用,得先握手:

  1. GET /sse 打开事件流,服务端回一个 endpoint 事件,里面带 sessionId 的 POST 地址(类似 /mcp/message?sessionId=xxx
  2. 拿到地址后,再 POST 你的 JSON-RPC 消息过去
  3. 响应从刚才那条 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_statusac-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,就是那篇的"改造前"。

小结

  1. 跑通比想象简单 :依赖 + 配置 + @Tool,三样凑齐就能用
  2. 声明式工具定义是甜点:业务方法加个注解变成 MCP 工具,协议演进不绑架你的代码
  3. 它现在是有状态的:下篇拆掉 session,看改动到底落在哪

仓库已经就位,git checkout v02 就能把这一篇的代码原样跑起来。先把这个最小骨架吃熟,后面几篇的改造才有落脚点。


本文完整可运行代码: github.com/ethanliang2016/mcp-in-action,对应目录 02-first-server,标签 v02git checkout v02 即可还原这一篇的状态。

MCP 实战手记系列路线图

# 篇目 状态
1 总纲篇:MCP 到哪一步了
2 跑通第一个 MCP Server(本篇)
3 把 MCP Server 改成无状态
4-7 CIMD 授权 / MCP Apps / 自建网关 / 安全篇 规划

后面几篇逐步放出,关注我,更新第一时间能看到

你跑第一个 MCP Server 时卡在哪一步?评论区说,有价值的我整理进后续篇目。


参考:

相关推荐
白远山1 小时前
上海24小时自助健身房系统软件开发实战指南:从需求到部署
java·架构·uni-app·需求分析
MayBaymax2 小时前
Spring AI Alibaba Graph 快速上手:黑板、节点、边
java·spring·ai·ai编程
斑鸠喳喳2 小时前
可重入锁 ReentrantLock
java·源码
IT枫斗者枫哥2 小时前
Spring AI 聊天记忆落库:重启后,怎样接上上一轮对话
java
devpotato2 小时前
HashMap 扩容机制:从源码细节到工程实践
java
yueping22 小时前
如何用idea打开jar包
java
IT_Octopus2 小时前
从一个应用开发者的角度,搞懂大数据查询的完整链路
java·大数据·数据库
许彰午2 小时前
51-BpmnDesigner集成
java·低代码·架构
蜗牛互联网2 小时前
Claude放宽生命科学限制:代价是验证、分级和30天留存
java·人工智能·后端