一、什么是MCP
引用一些官方的介绍吧:
Model Context Protocol(MCP) 是一个开放协议,它使LLM应用与外部数据源和工具之间的无缝集成成为可能。无论你是构建 AI 驱动的 IDE、改善 chat 交互,还是构建自定义的 AI 工作流,MCP 提供了一种标准化的方式,将 LLM 与它们所需的上下文连接起来。
大白话就是一个数据通信的应用协议,约定了应用和大模型之间如何传递数据进行无缝连接。
本文主要讲的是MCP的SSE+HTTP方式的使用。
先举个荔枝吧:)
二、当下背景
- 服务器通过
Ollama部署了一些乱七八糟的模型,用于提供给公司内部的朋友们使用。 - 另一台服务器上有一个公司内部的
ERP系统,管理着公司大量的数据信息。 - 你从隔壁社区听到了MCP的概念。
那我们能在这个背景下玩一些什么事情呢?
先看截图:
我们使用的客户端是CherryStudio,左边是我们的
ERP系统,右边是Ollama跑的一个小7B的通义千问开源模型。
我们直接通过CherryStudio的MCP协议接入功能,直接和ERP系统进行通信,实现ERP系统的数据查询和操作。
如果我们把CherryStudio换成手机上的Siri,身边的小爱同学呢?
Siri 可以通过快捷指令来完成,小爱同学可以通过小爱技能来完成,当然,体验肯定没有直接内置 MCP 来得快体验好。
三、着手分析
首先,我们先了解一下MCP的架构设计时序图:
UserCherryStudioServerOllama打开软件**SSE** 兄弟,我们聊会**SSE** 好,你有事的话 POST 这个地址(endpoint)**POST** 兄弟,自我介绍一下(initalize)**SSE** 好,这是我的基本信息(serverInfo)**POST** 兄弟,我收到了,我准备好了(initialized)POST: 兄弟,你有MCP的工具吗(tools/list)**SSE** 我提供了几个工具(tools)输入: 禁用张三的账号POST: 带工具调用 `禁用张三的账号`意图识别: {工具:禁用账号,参数:张三}**POST** 请求发送 {工具:禁用账号,参数:张三}执行工具并 **SSE** 推送结果整理下收到的结果返回处理后的结果显示给用户看UserCherryStudioServerOllama
四、开始开发
有了架构图了,那开发起来倒是没有什么难事了:
当然,你可以使用官网提供的一些SDK来做,不过吧,很多问题,你可以先试试了来评论区讨论~。。。
我们就不考虑上SDK啦,直接在项目里生撸!
4.1 项目技术栈
- 运行时:Java17
- 框架:SpringBoot
- ORM:JPA
来吧,直接开始。
4.2MCP的基础数据结构
4.2.1 基础结构
json
体验AI代码助手
代码解读
复制代码
{ "id": 0, "jsonrpc": "2.0" }
4.2.2 请求结构
所有发送给MCP服务器的请求都是这个结构:
ts
体验AI代码助手
代码解读
复制代码
interface Request { // 请求的ID id: number // 请求的协议 固定2.0 jsonrpc: "2.0"; // 请求的方法 method: string; // 请求的参数 params?: { ... }; }
例如 方法initalize的请求结构:
json
体验AI代码助手
代码解读
复制代码
{ "id": 0, "jsonrpc": "2.0", "method": "initalize", "params": { // 客户端的一些能力 "capabilities": {}, "clientInfo": { // 一些客户端信息,比如名称、版本等 } } }
又例如 函数调用的 请求结构
json
体验AI代码助手
代码解读
复制代码
{ "id": 1, "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "disableUserByName", "arguments": { "name": "张三" } } }
4.2.3 响应结构
所有通过SSE推送给客户端的响应都是这个结构:
ts
体验AI代码助手
代码解读
复制代码
interface Response { id: 0; jsonrpc: "2.0"; result: { // 一些数据信息 }; error: { // 一些错误信息 }; }
4.3 SSE 服务
SpringBoot 下开启一个SSE服务简单得不要不要的:
java
体验AI代码助手
代码解读
复制代码
public final static ConcurrentHashMap<String, SseEmitter> EMITTERS = new ConcurrentHashMap<>(); @GetMapping(value = "sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter connect() throws IOException { String uuid = UUID.randomUUID().toString(); SseEmitter emitter = new SseEmitter(); sseEmitter.send(SseEmitter.event() .name("endpoint") .data("/mcp/messages?sessionId=" + uuid) .build() ); EMITTERS.put(uuid, emitter); // 可以加点心跳 emitter.onCompletion(() -> EMITTERS.remove(uuid)); emitter.onTimeout(() -> EMITTERS.remove(uuid)); return emitter; return sseEmitter; }
这里需要注意的是,MCP要求连接上后必须发送一次消息,内容是MCP服务用于接受POST请求的 URL。
好,这个服务有了,客户端就可以通过这个服务来收我们要下发的消息了。
4.4 Message POST API
接下来,我们来实现这个复杂一点的POST请求:
java
体验AI代码助手
代码解读
复制代码
@PostMapping("messages") public Json messages(HttpServletRequest request, @RequestBody McpRequest mcpRequest) { String uuid = request.getParameter("sessionId"); if (Objects.isNull(uuid)) { return Json.error("sessionId is required"); } String method = mcpRequest.getMethod(); switch(method){ case "initalize": // 这个请求是初始化请求,需要返回一些服务器信息给客户端 break; case "tools/call": // 这个请求是工具调用请求,需要返回执行结果给客户端 break; case "tools/list": // 这个请求是工具列表请求,需要返回一些工具列表给客户端 break; default: } }
请注意,所有请求都不是 HTTP 直接响应,而是通过刚才的SSE通道推送回去。
4.4.1 initalize 初始化
初始化请求需要响应给客户端的是服务器的一些基本信息:
json
体验AI代码助手
代码解读
复制代码
{ id: id, jsonrpc: "2.0", result: { // 一些服务能力 capabilities: {}, serverInfo: { name: "服务器名称", version: "1.0.0" } } }
这时候,客户端已经可以显示服务器的基本信息了。
4.4.2 请求工具列表
SSE服务器收到到请求后,需要响应给客户端的是工具列表:
json
体验AI代码助手
代码解读
复制代码
{ "id": 0, "jsonrpc": "2.0", "result": { "tools": [ { "name": "disableUserByName", "description": "禁用一个用户的账号", "inputSchema": { "type": "object", "properties": { "nickname": { "type": "string", "description": "名称" } }, "required": ["nickname"] } } ] } }
4.4.3 执行工具
SSE服务器需要执行工具时,会得到这个结构体:
json
体验AI代码助手
代码解读
复制代码
{ "id": 1, "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "disableUserByName", "arguments": { "name": "张三" } } }
你可以在执行一些代码后,返回下面的结构体:
json
体验AI代码助手
代码解读
复制代码
{ "id": 1, "jsonrpc": "2.0", "result": { "content": [ { "type": "text", "text": "好,张三被我干掉了" } ] } }
到这里,几乎完成了整个流程。
4.5 基于注解的封装
我们因为使用的Java和SpringBoot, 所以我们使用了@McpMethod注解配合Reflections来实现自动注册工具。
java
体验AI代码助手
代码解读
复制代码
@McpMethod("modifyEmailByName") @Description("modify user new email by name") public String modifyEmailByName( @Description("the name of user, e.g. 凌小云") String name, @Description("the new email of user, e.g. example@domain.com") String email ) { List<UserEntity> userList = filter(new UserEntity().setNickname(name)); DATA_NOT_FOUND.when(userList.isEmpty(), "没有叫 " + name + " 的用户"); userList.forEach(user -> { updateToDatabase(get(user.getId()).setEmail(email)); }); return "已经将 " + userList.size() + " 个叫 " + name + " 的用户邮箱修改为 " + email; }
只要标记了@McpMethod注解,MCP服务器就会自动注册这个方法。
然后你就可以通过CherryStudio等工具来调用这个方法了。
动动嘴的事情~
五、总结
我们通过上述的方式完成了一个的MCP服务, 并且也可以为我们的一些其他系统进行扩展,用大模型来改造这些系统的使用方式,美滋滋。
当然,这里还有很多问题需要我们解决,比如权限控制。
完整的代码我们放在了我们的SPMS_Server项目以及AirPower4J基础库里了:
- SPMS-Server: github.com/s-pms/SPMS-…
- AirPower4J: github.com/HammCn/AirP…
六、展望
我倒是很悲观,现在满脑子都是小爱同学,把张三的辞职报告审核通过一下。
等各种终端设备都支持MCP协议了,我们再来玩更多的事情吧。
各位周末愉快,Bye.