|
"会调接口"早已不是后端工程师的专利——在AI时代,这成了每个想用大模型创造业务价值的Agent开发者必备技能。通过MCP协议让Agent获取业务上下文,已成为行业标配,集团也提供了完善的工具链支持。但当你真正想弄懂MCP时,官网白皮书再精美,也逃不过"一看就懂,一写就懵"的困境。 通过这篇文章可以学习以下内容: 手把手带你从0到1实现MCP Server; 全量剖析MCP协议及背后涉及的技术原理; 让你彻底告别"只听说过MCP"的尴尬;
Q:MCP 究竟是什么? A:MCP(Model Context Protocol,模型上下文协议)是规范应用程序向大语言模型提供上下文的开放协议。当AI客户端(MCP Host)需要获取上下文时,会通过集成的MCP Client向MCP Server发送请求,由Server返回模型所需的上下文数据。该协议的核心价值在于标准化交互流程——反之,如果AI客户端都不集成MCP Client,那该协议就和AI没有任何关系了。 📌 一句话:MCP本质上是MCP Client和MCP Server之间的通信协议,实现了Agent开发与工具开发的解耦。
Q:协议具体如何运作 A:由三个核心组件协作: 1.MCP Client:负责发送请求/接收响应
2.MCP Server:处理请求并返回上下文数据
3.MCP Host:MCP协议的执行者。负责:
接收用户问题 → 选择工具 → 构建参数 → 通过Client调用Server → 解析结果 → 继续对话 📌 集成MCP Client的智能体执行平台(如 IDEA LAB)或模型厂商的 Agent/AI 客户端(如 Cherry Studio)均可承担MCP Host职能。
Q:MCP Host、MCP Server、MCP Client到底长什么样啊? 以阿里云百练平台为例,MCP Host可以简单理解为平台上的智能体应用 MCP Host长这样子: MCP Client直接集成在MCP Host中了,所以我们是看不到的。但是我们可以看看自己写一个MCP Client的例子: @Slf4jpublicclassMcpClient{privateStringbaseUri;//MCP服务器基础地址privateStringsseEndpoint;//SSE连接端点privateStringmessageEndpoint;//HTTPPOST消息端点privateSseClientsseClient=newSseClient();//SSE客户端实例//异步任务管理-用于同步连接状态和请求响应privateCompletableFuture<Void>connectFuture=newCompletableFuture<>();//连接建立完成信号privateCompletableFuture<Void>initFuture=newCompletableFuture<>();//初始化完成信号//待处理的请求映射表:requestId->对应的Future对象privatefinalMap<String,CompletableFuture<Map<String,Object>>>pendingRequests=newConcurrentHashMap<>();privatefinalObjectMapperobjectMapper=newObjectMapper();//JSON序列化工具/***MCP客户端构造函数*@parambaseUriMCP服务器地址*/publicMcpClient(StringbaseUri){this.baseUri=baseUri;this.sseEndpoint=baseUri;//MCP协议规定的三步初始化流程this.connect();//1.建立SSE连接,获取POST端点this.sendInitialize();//2.发送初始化请求,协商协议版本和能力this.sendNotification("notifications/initialized",null);//3.发送初始化完成通知}}MCP Server长这样子: MCP Host上注册好MCP Server以后,我们这个智能体应用就可以调用MCP Server了。 Q:那这个协议和Type-C接口、统一function call有什么关系呢? A:当所有的Agent/AI客户端集成了MCP Client,并且通过MCP协议去实现调用ingFang SC", system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;letter-spacing: 0.034em;font-style: normal;font-weight: normal;">MCP Server提供的工具,从Agent开发的角度来说,MCP就变成了一个Agent进行function call的协议了,如果大家都按MCP的方式去给Agent添加工具,不就是统一了function call吗?所以如果把Agent比作手机,接口比作充电线,那ingFang SC", system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;letter-spacing: 0.034em;font-style: normal;font-weight: normal;">MCP Server牌充电线不就是Type-C接口类型的充电线了吗? 1. 让模型调用接口更简单,把接口上下文一键喂给LLM 有了统一协议,IDEA LAB、通义千问等平台的AI客户端(MCP Host)内置MCP Client,按协议通信。我们只需把自己的接口封装成ingFang SC", system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;letter-spacing: 0.034em;font-style: normal;font-weight: normal;">MCP Server,接入平台即可;选工具、拼参数、调接口、解析结果等链路由平台自动完成。 2. 实现了Agent开发与工具开发的解耦,类似于前后端解耦的意义 所有平台支持MCP协议后,工具按MCP Server标准发布,Agent即可直接调用,无需再关心"选-拼-调-解"全流程,真正做到工具开发与Agent开发分离。 1. SSE(Server-Sent Events):MCP Client和MCP Server之间的数据传输方式SSE全称Server-Sent Events,是一种基于HTTP协议实现的Web技术,专门用于服务器向客户端实时推送数据。它通过建立持久的HTTP连接,让客户端监听事件流,从而实时接收服务器推送的数据。 那么sse是如何做到实时推送的呢? 其实,SSE采用了一种巧妙的变通方法:虽然底层仍然是请求-响应模式,但响应的不是一个简单的数据包,而是一个持续不断的流信息(stream)。这就像是一次耗时很长的下载过程,让数据源源不断地流向客户端。正因为SSE基于HTTP实现,所以浏览器天然支持这一技术,无需额外配置。 【一句话结论】 在 MCP 体系里,SSE 就是"用一次超长时间的 HTTP 下载"来伪装成服务器主动推送;规范极简(4 个固定字段),浏览器原生支持,Spring 既可用同步阻塞的SseEmitter,也可用响应式的Flux<ServerSentEvent>实现。 一、SSE在MCP中的定位1. 定位:实现MCP Server → MCP Client 的单向实时数据流,解决「怎么传」- 长连接 +
text/event-stream响应,把"请求-响应"拉成"长时间下载"。
二、协议格式(只需记 4 个字段)Header Content-Type:text/event-streamCache-Control:no-cacheConnection:keep-alive Body field:value\n\n/*每条消息以两个换行结束*/ 服务器发送给浏览器的body必须按上述k-v格式符合,其中field的类型是固定的四种字段: event事件类型,前端addEventListener('event名')
MCP 实战示例 MCP Server返回给MCP Client的SSE数据包如下: event: endpointdata: /message?sessionId=fc92c0fc-6222-466c-b353-bdc4931980a6
event: messagedata: {"jsonrpc":"2.0","id":"0","result":{"capabilities":{"tools":{"listChanged":true}},"serverInfo":{"name":"SpringBoot MCP Server","version":"1.0.0"},"protocolVersion":"2024-11-05"}}
event: messagedata: {"jsonrpc":"2.0","id":"1","result":{"tools":[{"inputSchema":{"type":"object","properties":{"name":{"description":"Name to greet (optional)","type":"string"}}},"name":"hello_world","description":"Returns a Hello World message"},{"inputSchema":{"type":"object","properties":{}},"name":"get_time","description":"Returns current server time"},{"inputSchema":{"type":"object","properties":{"message":{"description":"Message to echo back","type":"string"}}},"name":"echo","description":"Echoes back the provided message"}]}}
event: messagedata: {"jsonrpc":"2.0","id":"2","result":{"content":[{"text":"Hello, 宸游!","type":"text"}]}}
三、实现路线对比四、代码级 DemoJava实现SSE方式一:Spring MVC(阻塞式) 通过返回一个SseEmitter对象作为事件发射器给到前端,然后我们在后端调用emitter.send()方法发送的数据会被封装为SSE事件流的形式,前端可以通过监听该事件流来进行数据更新。 @GetMapping(value="/stock-price",produces=TEXT_EVENT_STREAM_VALUE)publicSseEmitterstream(){SseEmittere=newSseEmitter();newThread(()->{while(true){e.send(SseEmitter.event().data(price));sleep(1000);}}).start();returne;}前端 在js中通过new EventSource建立一个EventSource对象,它会与"/stock-price"这个路径建立sse长连接,并监听事件。当后端发送event事件以后,会触发eventSource.onmessage这个回调函数,将数据写入到页面中。 constes=newEventSource("/stock-price");es.onmessage=e=>document.body.innerText=e.data;Java实现SSE方式二:Spring WebFlux(响应式) 返回publisher给到前端进行订阅,然后我们在后端通过Sinks.Many<ServerSentEvent<?>>这个类的tryEmitNext方法来手动发送事件给到前端,前端可以通过监听该事件流来进行数据更新。 @GetMapping("/stream-sse")publicFlux<ServerSentEvent<String>>stream(){returnFlux.interval(Duration.ofSeconds(1)).map(i->ServerSentEvent.builder().id(i.toString()).event("tick").data("now="+LocalTime.now()).build());}前端同上。 2. JSON-RPC 2.0:MCP Client和MCP Server之间的传输的内容格式JSON-RPC 2.0 是一种无状态、轻量级的远程过程调用(RPC)协议,定义了MCP Client和MCP Server之间进行通信的格式。JSON-RPC 2.0协议定义了请求和响应的格式,以及错误处理机制。 【一句话结论】 在 MCP 里,Client与 Server永远只交换一条"四字段" JSON-RPC 2.0 消息:版本ingFang SC", system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;letter-spacing: 0.034em;font-style: normal;font-weight: normal;">jsonrpc、方法ingFang SC", system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;letter-spacing: 0.034em;font-style: normal;font-weight: normal;">method、编号ingFang SC", system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;letter-spacing: 0.034em;font-style: normal;font-weight: normal;">id、载荷ingFang SC", system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;letter-spacing: 0.034em;font-style: normal;font-weight: normal;">params/result|error 一、JSON-RPC 2.0 在 MCP 中的定位1. 定位:定义MCP Server和MCP Client之间的统一的无状态消息格式,解决「说什么」2. 本质:通过无状态、轻量级的四字段 JSON 契约,实现两端零差异解析。二、消息骨架(记住 4 个固定键)三、完整范式1. 请求 {"jsonrpc":"2.0","method":"...","params":{...},"id":<unique>}2. 响应 {"jsonrpc":"2.0","id":<same>,"result":{...}}//成功{"jsonrpc":"2.0","id":<same>,"error":{code,message,data?}}//失败四、MCP 实战示例:initialize请求客户端 → 服务器 {"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{"sampling":{},"roots":{"listChanged":true}},"clientInfo":{"name":"mcp-inspector","version":"0.9.0"}},"id":0}服务器回同样id的响应,宣告双方能力,即可进入正式会话。 3. MCP协议:MCP Client和MCP Server之间的通信(调用)协议MCP本质上是MCP Client和MCP Server之间的通信协议,那么这个MCP协议具体定义了写什么内容呢? 1. 定义了MCP Server和MCP Client之间的连接建立过程;2. 定义了MCP Client和MCP Server之间如何进行信息(上下文)交换;3. 定义了MCP Client和MCP Server之间如何连接关闭过程;【一句话结论】 MCP 协议就是"客户端先拉一条 SSE 长连接收通知,再用普通 HTTP POST 发请求到服务端;Client和Server之间必须先发 initialize 打招呼,才能调工具,最后Client关 SSE 就通信结束了"。 一、协议定位 & 本质定位:在 SSE + JSON-RPC 2.0 之上,定义两通道、四步骤、全生命周期的MCP Server与 MCP Client 通信协议 本质:把"怎么连、怎么握手、怎么调工具、怎么结束"写死成固定流程,MCP Host只需照表填空,就可以实现LLM获取MCP Server提供的上下文。 二、MCP Server 和 MCP Client 之间的通信"两通道"服务器启动时必须同时暴露这两个端点,缺一不可。 三、MCP Server 和 MCP Client 之间的通信"四步骤"1.连 客户端 发送GET /sse请求,建立sse长连接 ,用于后续服务端推送数据给客户端;2.取 服务器回event: endpoint的消息,给出后续客户端发送信息给服务端的POST 端点 URI;3.握 客户端向该 URI 发两包 JSON-RPC;
initialize请求 → 服务器返回 capabilities;notifications/initialized通知 → 握手完成;
四、MCP Server 和 MCP Client 之间的通信"生命周期"
Talk is cheap, show me the code。当我们尝试实现一个MCP Server时,我们就会对MCP彻底搞懂,而不是仅仅一个写在纸上的协议了。那么怎么实现一个MCP Server呢? 任何语言/框架实现 MCP Server 时只要: 3. 实现通信"四步骤"所需要的接口,即可开发出一个MCP Server。下面我们将通过具体的代码示例来实现一个简单的MCP Server: - 技术栈:Spring Boot + WebFlux + Jackson
springboot-mcp-server/├──pom.xml├──src/│├──main/││├──java/│││└──com/│││└──example/│││├──HelloWorldApplication.java│││├──config/--http配置││││└──McpConfig.java│││└──mcp/│││├──protocol/--jsonrpc2.0格式的请求和响应││││├──McpRequest.java││││└──McpResponse.java│││├──transport/--sse与httppost传输端点││││└──SseTransport.java│││├──server/--mcp协议处理││││└──McpServerHandler.java│││└──tools/--工具注册、调用│││└──McpToolRegistry.java││└──resources/││├──application.properties││└──static/││└──mcp-test.html│└──test/│└──java/│└──com/│└──example/│└──HelloWorldApplicationTests.java└──README.md 2. 实现核心组件:两通道的构建(SseTransport) SSE端点实现:GET/sse创建SSE长连接端点,用于服务器向客户端推送数据: /***标准MCPSSE端点-符合MCP协议规范*客户端连接此端点接收服务器消息*/@GetMapping(value="/sse",produces=MediaType.TEXT_EVENT_STREAM_VALUE)publicFlux<ServerSentEvent<String>>sseEndpoint(@RequestParam(required=false)StringclientId,ServerHttpRequestrequest){//安全检查:验证Origin头防止DNS重绑定攻击Stringorigin=request.getHeaders().getFirst("Origin");if(origin!=null&&!isValidOrigin(origin)){System.err.println("Invalidoriginrejected:"+origin);returnFlux.error(newSecurityException("Invalidorigin"));}StringsessionId=clientId!=null?clientId:"client-"+System.currentTimeMillis();//为每个客户端创建独立的消息流Sinks.Many<ServerSentEvent<String>>sink=Sinks.many().multicast().onBackpressureBuffer();clientSinks.put(sessionId,sink);System.out.println("MCPSSEclientconnected:"+sessionId+"fromorigin:"+origin);//创建心跳流,每30秒发送一次心跳Flux<ServerSentEvent<String>>heartbeat=Flux.interval(Duration.ofSeconds(30)).map(tick->ServerSentEvent.<String>builder().event("ping").data("{\"type\":\"ping\"}").build());//合并消息流和心跳流returnFlux.merge(sink.asFlux(),heartbeat).doOnSubscribe(subscription->{//根据MCP协议,必须发送endpoint事件try{//1.发送endpoint事件-MCP协议要求StringendpointUri=getBaseUrl(request)+"/message/"+sessionId;//直接发送URI字符串,不包装在对象中ServerSentEvent<String>endpointEvent=ServerSentEvent.<String>builder().event("endpoint").data(endpointUri).build();sink.tryEmitNext(endpointEvent);System.out.println("Sentendpointeventtoclient:"+sessionId+"withURI:"+endpointUri);}catch(Exceptione){System.err.println("Errorsendingendpointevent:"+e.getMessage());}}).doOnCancel(()->{System.out.println("MCPSSEclientdisconnected:"+sessionId);clientSinks.remove(sessionId);sink.tryEmitComplete();}).doOnError(error->{System.err.println("MCPSSEerrorforclient"+sessionId+":"+error.getMessage());clientSinks.remove(sessionId);}).onErrorResume(error->{System.err.println("SSEstreamerror,attemptingtorecover:"+error.getMessage());returnFlux.empty();});}HTTP POST端点实现:POST/message/{sessionId}创建处理客户端请求的端点 /*** 处理客户端发送的 MCP 请求 - 支持会话特定端点* 客户端通过 POST 请求发送 MCP 消息到指定的端点*/@PostMapping(value ="/message/{sessionId}", consumes =MediaType.APPLICATION_JSON_VALUE)publicMono<Void>handleSessionMessage(@PathVariableStringsessionId, @RequestBodyStringmessageJson, ServerHttpRequestrequest) { returnhandleMessageInternal(sessionId, messageJson, request);}
privateMono<Void>handleMessageInternal(StringsessionId,StringmessageJson,ServerHttpRequestrequest){ returnMono.fromRunnable(() -> { try{ // 安全检查:验证 Origin 头 Stringorigin = request.getHeaders().getFirst("Origin"); if(origin !=null&& !isValidOrigin(origin)) { System.err.println("Invalid origin rejected for message: "+ origin); return; } // 解析消息 McpRequestmcpRequest = objectMapper.readValue(messageJson,McpRequest.class); System.out.println("Received MCP request: "+ mcpRequest.getMethod() + " (id: "+ mcpRequest.getId() +") from session: "+ sessionId); // 处理请求 McpResponseresponse = serverHandler.handleRequest(mcpRequest); // 通过 SSE 发送响应 if(response !=null) { sendMessageToClient(sessionId, response); } }catch(Exceptione) { System.err.println("Error processing MCP message: "+ e.getMessage()); e.printStackTrace(); // 发送错误响应 try{ McpRequestmcpReq = objectMapper.readValue(messageJson,McpRequest.class); McpResponseerrorResponse =newMcpResponse(); errorResponse.setId(mcpReq.getId()); errorResponse.setError(newMcpResponse.McpError(-32603,"Internal error: "+ e.getMessage())); sendMessageToClient(sessionId, errorResponse); }catch(Exceptionex) { System.err.println("Error sending error response: "+ ex.getMessage()); } } });}
/*** 向指定客户端发送 MCP 消息*/publicvoidsendMessageToClient(StringsessionId,Objectmessage){ Sinks.Many<ServerSentEvent<String>> sink = clientSinks.get(sessionId); if(sink !=null) { try{ StringjsonData = objectMapper.writeValueAsString(message); ServerSentEvent<String> event =ServerSentEvent.<String>builder() .event("message") .data(jsonData) .build(); sink.tryEmitNext(event); System.out.println("Sent MCP message to client: "+ sessionId); }catch(Exceptione) { System.err.println("Error sending message to client "+ sessionId +": "+ e.getMessage()); } }else{ System.err.println("No active connection found for session: "+ sessionId); }}
3. 实现通信"四步骤"所需要的接口 (McpServerHandler) 呼应前面讲到的MCP"四步骤"生命周期: 步骤一:建立连接(连)步骤二:获取端点信息(取)- 服务器通过SSE流发送
event: endpoint事件
步骤三:初始化握手(握)- 处理
notifications/initialized通知
步骤四:工具调用(用)- 实现
tools/call接口,处理具体工具调用请求
在McpServerHandler中,处理请求的方法handleRequest(),实现了步骤三和步骤四所需要的接口和逻辑。 publicMcpResponsehandleRequest(McpRequestrequest){Stringmethod=request.getMethod();Stringid=request.getId();Map<String,Object>params=request.getParams();try{switch(method){case"initialize":returnhandleInitialize(id,params);case"tools/list":returnhandleListTools(id);case"tools/call":returnhandleCallTool(id,params);case"notifications/initialized"://客户端初始化完成通知,不需要响应this.initialized=true;System.out.println("Clientinitializationcompleted");returnnull;default://创建错误响应,确保只包含error字段,不包含resultMcpResponseerrorResponse=newMcpResponse();errorResponse.setId(id);errorResponse.setError(newMcpResponse.McpError(-32601,"Methodnotfound:"+method));returnerrorResponse;}}catch(Exceptione){System.err.println("Errorhandlingrequest"+method+":"+e.getMessage());//创建错误响应,确保只包含error字段,不包含resultMcpResponseerrorResponse=newMcpResponse();errorResponse.setId(id);errorResponse.setError(newMcpResponse.McpError(-32603,"Internalerror:"+e.getMessage()));returnerrorResponse;}}使用 MCP Inspector1. 启动服务器:mvn spring-boot:run2. 打开 MCP Inspector v0.9.04. 输入 URL:http://localhost:8080/sse📌MCP Inspector只需要安装好node.js环境以后直接运行命令:npx "@modelcontextprotocol/inspector@0.9"即可
使用内置测试客户端1. 访问:http://localhost:8080/mcp-test.html2. 点击 "Connect to MCP Server"通过以上代码实现,我们就完成了一个基本的MCP Server,它能够: 1. 通过SSE端点建立连接并发送endpoint事件;2. 通过POST端点处理initialize、tools/list、tools/call等请求;这与我们前面介绍的MCP协议的"两通道"和"四步骤"完全对应,帮助我们深入理解MCP的工作原理。 MCP作为连接Agent与工具的桥梁,正在重塑大模型应用开发的格局。掌握MCP技术原理,将帮助我们在Agent开发的道路上走得更远。 |