AI工程 | MCP实战开发

51 阅读9分钟

AI工程 | MCP实战开发

导读

书接上文AI工程 | MCP是怎么跟大模型交互的?本篇将通过代码来继续拆解MCP架构落地时的第2、3、4个问题。【AI大模型教程】

  1. MCP和大模型怎么交互?
  2. Client和Server如何通信?
  3. 如何开发MCP Server?
  4. 如何开发MCP Client?

Client和Server如何通信?

Cilent和Server的通信可以说是MCP架构最关键的部分。通过前文我们知道构成MCP完整架构的三个部分:MCP Host、MCP Client、MCP Host。

MCP Host服务就是我们常说的开发一个AI应用,而MCP Server一般是别人的程序,MCP Client非独立服务,包含在MCP Host服务里。因此,MCP的Client和Server之间通信其实是MCP Host和MCP Server两个服务之间的通信。

C和S之间通信可以类比服务端编程领域的RPC(远程进程调用)通信,如gRPC、Dubbo、JSON-RPC。RPC通信有几个重要部分包括数据格式、传输协议、服务发现。下面从这三个方面来看下MCP的C/S通信细节:

  • 数据格式:MCP的C/S的数据格式是标准的JSON-RPC 2.0
  • 传输协议:
  • Stdio:C/S都在同一个机器上,Server作为子进程启动,通过操作系统的标准stdin输入和stdout输出进行数据交互
  • SSE:Server-Send-Event,基于HTTP协议、利用 HTTP 长连接实现服务器向客户端单向持续推送内容
  • Streamable Http:基于HTTP协议,以普通 HTTP 请求为基础,服务器可按需将响应升级为 SSE 流,支持无状态服务器
  • 服务发现:
  • Stdio:不涉及网络调用,Client仅通过进程名称找到Server
  • Streamable Http:通过域名发现,Server通过通知类型消息(notification)告知Client其状态变化,比如Server上的工具列表更新

三种传输协议怎么选?简单来说,本地通信用Stdio(比如在安全隐私要求高的设备上如汽车、手机,离线环境下的CI/CD脚本、工具链、插件等),远程网络通信用Streamable Http和SSE,但Streamable Http可以替代SSE且更具优势,官方已经不建议再使用SSE。

为什么MCP会推荐Steamable Http而弃用SSE?

Streamable Http和SSE都是基于HTTP协议的,前者是http短连接、后者是http长连接事件流协议(格式为text/event-stream),因此MCP推荐用前者的大部分理由都是因为短连接和长连接的区别带来的。

大部分我们接触到的网络请求都是客户端(Client)发起一次性请求并从服务端(Server)获取结果的短连接形式,数据是从服务端单方向传输到客户端。长连接适合客户端和服务端两个方向都可以发起请求,传输数据。但长连接会带来高得多的开发难度和运维成本。

从3个方面回答为什么MCP推荐用Streamable Http?

1、合理评估长耗时任务

我们知道大模型厂商的API绝大多数默认采用SSE协议的。因为大模型的推理和生成往往耗时比较久,为了让调用方尽快看到一部分内容,只能一批一批地返回,也叫流式输出。换句话说SSE是长耗时任务的常见选型。但MCP server大多承担工具执行角色,返回给MCP client的内容其实大部分不是长耗时的。从八二法则来看,采用Streamable Http会更合算。

当然遇到server需要给client推送内容时(即notification消息),Streamable Http内部会临时升级为SSE。

2、长连接的有状态。用SSE意味着server是有连接状态的。做服务端开发的,特别是负责系统运维的同学,一定特别不喜欢“有状态”这个词。这意味着你会遇到很多不顺心的事。

SSE长连接会拖慢响应时间、消耗额外服务器资源、增加部署硬件成本。部署的时候也要求在负载均衡反向代理层需要特殊的配置,带来运维复杂度。应对请求流量变化时也不利于弹性扩缩容。

3、双端点的不便。SSE需要维护两个通信通道(/see的GET用于接受响应,/messaged的POST用于发送请求),双端点带来复杂性。

当然MCP Streamable Http传输协议毕竟不是简单的REST API这种简单的请求-响应型。其实服务端也维护了session会话,断连后的续连就是靠会话ID来做到。

下面结合代码示例进一步理解三种协议是如何工作的。注:本篇示例代码采用Go语言,基于开源mcp sdk:github.com/mark3labs/mcp-go

如何开发MCP Server?

假设MCP Server叫mcp-server-demo,定义了一个工具hello_world,可以给传入的名字name打招呼。代码示例如下(完整的Server代码可以在github上查看:github.com/kobelv/mcp-…

func NewMCPServer() *server.MCPServer {
s := server.NewMCPServer("mcp-server-demo", "0.1.0", server.WithToolCapabilities(false), server.WithRecovery())
t := mcp.NewTool("hellow_world",
mcp.WithDescription("say hello to someone"),
mcp.WithString("name", mcp.Required(), mcp.Description("name from the person to greet")),
)
s.AddTool(t, GreetHandler)
return s
}
func GreetHandler(_ context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
name, err := req.RequireString("name")
if err != nil {
return mcp.NewToolResultError(err.Error()), errors.New("name must be a string")
}
return mcp.NewToolResultText(fmt.Sprintf("Hello, %s!", name)), nil
}

Stdio协议代码示例

server启动:
if err := server.ServeStdio(NewMCPServer()); err != nil {
log.Fatalf("Stdio MCP Server error: %v", err)
}
通过mcp官方Inspector工具测试:

使用mcp官方的inspector工具,可以不用自己编写client程序很方便测试mcp server。

  1. 首先确保你的机器环境可以运行npx,通过命令行远程运行Inspector工具:npx @modelcontextprotocol/inspector

  2. 正确填写mcp server信息,传输方式选择STDIO,根据server启动情况按需填写Command、Argumens、Environment Variables。比如上面示例是go程序,在Command框里填入执行文件的全路径就可以了

  3. 点击Connect连接到server,可以查看和执行工具

  4. 执行hellow_world工具输入“给飞邪问好”,Too Result呈现了工具运行的结果:“Hello,飞邪”

Streamable Http 协议代码示例

server启动:
httpServer := server.NewStreamableHTTPServer(NewMCPServer())
log.Printf("HTTP server is listening on :8080/mcp")
if err := httpServer.Start(":8080"); err != nil {
log.Fatalf("Server error: %v", err)
}
通过Inspector测试:

通过Claude Desktop测试:

有很多桌面版的大模型工具都可以很方便地安装mcp server,比如claude desktop、cursor等。下面以claude desktop举例。

在测试之前,让我们把上面的hello_world工具能力变强一点,可以接受三个参数:打招呼的人名、内容、日期。

t := mcp.NewTool("hello_world",
mcp.WithDescription("say hello to someone"),
mcp.WithString("greet_name", mcp.Required(), mcp.Description("name from the person to greet")),
mcp.WithString("greet_message", mcp.Description("message to greet"), mcp.DefaultString("have a good day")),
mcp.WithString("greet_date", mcp.Description("greet date"), mcp.DefaultString(time.Now().Format("2006/01/02"))),
)
// GreetHandler 是一个请求hello_world工具时的处理函数
func GreetHandler(_ context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
greet_name, err := req.RequireString("greet_name")
if err != nil {
return mcp.NewToolResultError(err.Error()), errors.New("greet_name must be a string")
}
greet_date, err := req.RequireString("greet_date")
if err != nil {
return mcp.NewToolResultError(err.Error()), errors.New("greet_date must be a string")
}
greet_message, _ := req.GetArguments()["greet_message"].(string)
return mcp.NewToolResultText(fmt.Sprintf("Hello %s, today is %s, %s", greet_name, greet_date, greet_message)), nil
}
  1. 安装Claude Desktop桌面版

  2. 到安装目录下找到claude_desktop_config.json并修改,command和args参数类似Inspector的配置

  3. 启动Claude Desktop,可以看到加载了mcp-server-demo

  4. 测试:输入对话内容:“给全世界问好,就说“再也不敢,谁也不敢,如今,中国人的天、地、海都是中国人自己做主”

  5. 可以看到Claude Desktop能够智能判断这句对话需要调用工具mcp-server-demo,并正常返回了工具处理结果

如何开发MCP Client?

上面都是MCP Server侧的代码,下面我们自己来开发一个Host,用MCP Client来连接Server。(完整的Host代码可以在github上查看:github.com/kobelv/mcp-…

client连接server示例代码:

  1. 通过/mcp端点来连接server
  2. 通过Initialize方法初始化
  3. 可以尝试ping测试连通性

注意:client在调用server之前必须先初始化才能做调用工具、提示词和资源,接收server通知等后续操作。

shttp, err := transport.NewStreamableHTTP("http://127.0.0.1:8080/mcp")
if err != nil {
fmt.Printf("failed to create streamable HTTP %v\n", err)
}
if err := shttp.Start(ctx); err != nil {
fmt.Printf("failed to start streamable HTTP %v\n", err)
}
c := client.NewClient(shttp)
req := mcp.InitializeRequest{}
req.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
req.Params.ClientInfo = mcp.Implementation{
Name:    "I'm MCP Client",
Version: "1.0.0",
}
req.Params.Capabilities = mcp.ClientCapabilities{}
serverInfo, err := c.Initialize(ctx, req)
if err != nil || serverInfo == nil {
fmt.Printf("failed to init client %v\n", err)
}
fmt.Printf("connected to server: %s, version is %s\n", serverInfo.ServerInfo.Name, serverInfo.ServerInfo.Version)
if err := c.Ping(ctx); err != nil {
fmt.Printf("failed to ping %v\n", err)
}

MCP Host主流程代码如下

  1. 启动程序时初始化client并连接server,如上述代码所示
  2. 请求server获取工具列表
  3. 组装工具列表及用户问题,调用大模型
  4. 解析大模型返回结果
  5. 发起工具调用
  6. 组装工具返回结果,再次调用大模型(本示例里省略了)
func (ds *ChatDS) Chat(ctx context.Context, entity *entity.ChatInputEntity) (any, error) {
// #1 list mcp server's tools
// #2 new LLM, invoke function call, compose functions by mcp server‘s tools via step 1
// #3 mcp client invoke mcp server tool call via step 2
tools := ds.mcpServerAdapter.ListMcpTools(ctx)
fcRes, err := ds.chatAdapter.InvokeFunctionCallArk(ctx, entity.Query, tools.Tools)
if err != nil {
return nil, err
}
toolRes, err := ds.mcpServerAdapter.InvokeMcpTool(ctx, fcRes)
if err != nil {
return nil, err
}
return toolRes.Content, nil
}

整个host模块是个完整的web服务端程序,运行起来后可以跟它问:“给Kobe带句话,就说“你的曼巴精神一直都在,R.I.P!!!”,经过上面一系列步骤后最后会调用hello_world工具返回“你好 Kobe, 今天是 2025/09/06, 你的曼巴精神一直都在,R.I.P!!!”。

当然你也可以问上面Claude Desktop示例中的问题,获得和上面Claude Desktop示例调用hello_world工具一样的返回结果。如果大模型表现正常的话,即使你换些类似的问法,大概率也能获得一样的答案,这就属于大模型的理解能力范畴了。

结语

MCP协议里server除了提供tools,还有prompt和resource,其技术本身没有区别所以本次就略过了。文中示例代码属于演示用途,实际项目里的tools数量更多,入参更多,涉及的server也更多,带来的工程复杂也高得多,属于本篇未尽事宜。

回头看MCP架构,就像我们说的冰山,水面以下才是大头。MCP架构其实也是,90%是软件工程,10%是AI。这个结论甚至可以推广到目前大家用到的其他AI应用的AI和工程的关系。

工程类技术的掌握之道就是动手实践。先看官网文档:modelcontextprotocol.io/docs/gettin… sdk代码。比如go语言,其官方的mcp sdk迭代节奏偏慢,还没有完全开发好,可以看社区其他开源框架,比如本文使用到的

github.com/mark3labs/m…