链载Ai

标题: MCP 很火,来看看我们直接给后台管理系统上一个 MCP? [打印本页]

作者: 链载Ai    时间: 1 小时前
标题: MCP 很火,来看看我们直接给后台管理系统上一个 MCP?

什么是MCP

引用一些官方的介绍吧:

Model Context Protocol(MCP) 是一个开放协议,它使LLM应用与外部数据源和工具之间的无缝集成成为可能。无论你是构建 AI 驱动的 IDE、改善 chat 交互,还是构建自定义的 AI 工作流,MCP 提供了一种标准化的方式,将 LLM 与它们所需的上下文连接起来。

大白话就是一个数据通信的应用协议,约定了应用和大模型之间如何传递数据进行无缝连接。

「本文主要讲的是MCPSSE+HTTP方式的使用。」

先举个荔枝吧:)


当下背景

  1. 服务器通过Ollama部署了一些乱七八糟的模型,用于提供给公司内部的朋友们使用。
  2. 另一台服务器上有一个公司内部的ERP系统,管理着公司大量的数据信息。
  3. 你从隔壁社区听到了「MCP」的概念。

那我们能在这个背景下玩一些什么事情呢?

先看截图:


我们使用的客户端是「CherryStudio」,左边是我们的ERP系统,右边是Ollama跑的一个小「7B」的通义千问开源模型。

我们直接通过「CherryStudio」MCP协议接入功能,直接和ERP系统进行通信,实现ERP系统的数据查询和操作。

如果我们把「CherryStudio」换成手机上的「Siri」,身边的小爱同学呢?

Siri 可以通过快捷指令来完成,小爱同学可以通过小爱技能来完成,当然,体验肯定没有直接内置 MCP 来得快体验好。


着手分析

首先,我们先了解一下「MCP」的架构设计时序图:

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(linesequenceDiagramparticipantUserasUserparticipantCherryStudioasCherryStudioparticipantServerasServerparticipantOllamaasOllamaUser->>CherryStudio:打开软件CherryStudio-->>Server:**SSE**兄弟,我们聊会Server--)CherryStudio:**SSE**好,你有事的话POST这个地址(endpoint)CherryStudio-->>Server:**POST**兄弟,自我介绍一下(initalize)Server--)CherryStudio:**SSE**好,这是我的基本信息(serverInfo)CherryStudio-->>Server:**POST**兄弟,我收到了,我准备好了(initialized)CherryStudio-->>ServerOST:兄弟,你有MCP的工具吗(tools/list)Server--)CherryStudio:**SSE**我提供了几个工具(tools)User->>CherryStudio:输入:禁用张三的账号CherryStudio->>OllamaOST:带工具调用`禁用张三的账号`Ollama-)CherryStudio:意图识别:{工具:禁用账号,参数:张三}CherryStudio-->>Server:**POST**请求发送{工具:禁用账号,参数:张三}Server-->>CherryStudio:执行工具并**SSE**推送结果CherryStudio->>Ollama:整理下收到的结果Ollama-)CherryStudio:返回处理后的结果CherryStudio-)User:显示给用户看


开始开发

有了架构图了,那开发起来倒是没有什么难事了:

当然,你可以使用官网提供的一些「SDK」来做,不过吧,很多问题,你可以先试试了来评论区讨论~。。。

我们就不考虑上「SDK」啦,直接在项目里生撸!


「项目技术栈」

来吧,直接开始。


「MCP」的基础数据结构


基础结构

ounter(lineounter(lineounter(lineounter(line{"id":0,"jsonrpc":"2.0"}


请求结构 extends 基础结构

所有发送给「MCP」服务器的请求都是这个结构:

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineinterfaceRequest{// 请求的ID id: number
// 请求的协议 固定2.0 jsonrpc:"2.0";
// 请求的方法 method:string;
// 请求的参数params?: { ... };}

例如 方法initalize的请求结构:

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line{"id":0,"jsonrpc":"2.0","method":"initalize","params":{//客户端的一些能力"capabilities":{},"clientInfo":{//一些客户端信息,比如名称、版本等}}}

又例如 函数调用的 请求结构

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"name":"disableUserByName","arguments":{"name":"张三"}}}


响应结构 extends 基础结构

所有通过「SSE」推送给客户端的响应都是这个结构:

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineinterfaceResponse{id:0;jsonrpc:"2.0";result:{//一些数据信息};error:{//一些错误信息};}


SSE 服务

SpringBoot 下开启一个「SSE」服务简单得不要不要的:

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(linepublicfinalstaticConcurrentHashMap<String, SseEmitter> EMITTERS =newConcurrentHashMap<>();
@GetMapping(value = "sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)publicSseEmitterconnect()throwsIOException { Stringuuid=UUID.randomUUID().toString(); SseEmitteremitter=newSseEmitter(); 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)); returnemitter; returnsseEmitter;}

这里需要注意的是,「MCP」要求连接上后必须发送一次消息,内容是「MCP」服务用于接受「POST」请求的 URL。

好,这个服务有了,客户端就可以通过这个服务来收我们要下发的消息了。


Message POST API

接下来,我们来实现这个复杂一点的「POST」请求:

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line@PostMapping("messages")publicJsonmessages(HttpServletRequest request,@RequestBodyMcpRequest mcpRequest) { Stringuuid = request.getParameter("sessionId"); if(Objects.isNull(uuid)) {   returnJson.error("sessionId is required");  } Stringmethod = mcpRequest.getMethod();
switch(method){ case"initalize": // 这个请求是初始化请求,需要返回一些服务器信息给客户端 break; case"tools/call": // 这个请求是工具调用请求,需要返回执行结果给客户端 break; case"tools/list": // 这个请求是工具列表请求,需要返回一些工具列表给客户端 break; default: }}

请注意,所有请求都不是 HTTP 直接响应,而是通过刚才的「SSE」通道推送回去。


1、initalize 初始化

初始化请求需要响应给客户端的是服务器的一些基本信息:

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line{id:id,jsonrpc:"2.0",result:{//一些服务能力capabilities:{},serverInfo:{name:"服务器名称",version:"1.0.0"}}}

这时候,客户端已经可以显示服务器的基本信息了。


2、请求工具列表

「SSE」服务器收到到请求后,需要响应给客户端的是工具列表:

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line{"id":0,"jsonrpc":"2.0","result":{"tools":[{"name":"disableUserByName","description":"禁用一个用户的账号","inputSchema":{"type":"object","properties":{"nickname":{"type":"string","description":"名称"}},"required":["nickname"]}}]}}


3、执行工具

「SSE」服务器需要执行工具时,会得到这个结构体:

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"name":"disableUserByName","arguments":{"name":"张三"}}}

你可以在执行一些代码后,返回下面的结构体:

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line{"id":1,"jsonrpc":"2.0","result":{"content":[{"type":"text","text":"好,张三被我干掉了"}]}}

到这里,几乎完成了整个流程。


基于注解的封装

我们因为使用的「Java」「SpringBoot」, 所以我们使用了@McpMethod注解配合Reflections来实现自动注册工具。

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line@McpMethod("modifyEmailByName")@Description("modifyusernewemailbyname")publicStringmodifyEmailByName(@Description("thenameofuser,e.g.凌小云")Stringname,@Description("thenewemailofuser,e.g.example@domain.com")Stringemail){List<UserEntity>userList=filter(newUserEntity().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」服务, 并且也可以为我们的一些其他系统进行扩展,用大模型来改造这些系统的使用方式,美滋滋。

当然,这里还有很多问题需要我们解决,比如权限控制。






欢迎光临 链载Ai (https://www.lianzai.com/) Powered by Discuz! X3.5