golang mcp 服务端指南

Transport 对照表

本文是知识卡,不用从头读到尾。要选传输方式时看第一节的表;要抄某个 transport 的写法时跳到对应小节。

依赖 github.com/modelcontextprotocol/go-sdk v1.4.1(见 pkg.go.dev/mcp)。下面各段代码都是可以独立编译的 main 骨架,按需挑一种场景。

先纠正一个说法

平时聊天总说「stdio、HTTP、SSE 三种传输」,这是把概念混在一起了。MCP 官方标准里的传输就两种:

  1. stdio——客户端把服务端当子进程拉起来,往 stdin 写 JSON-RPC 消息、从 stdout 读,一行一条,所以消息体里不能有裸换行。日志走 stderr,千万别往 stdout 打,否则客户端解析直接乱掉。典型场景是本机 IDE(Cursor 之类)连本地服务:不暴露端口,也没有 Origin/CORS 这些麻烦。
  2. Streamable HTTP——服务端就是一个普通 HTTP 服务。客户端每次 POST 一条 JSON-RPC 消息,响应可能是普通 JSON,也可能是 SSE 流;服务端想主动往下推消息时,客户端可以另开一条 GET 的 SSE 连接,会话用 MCP-Session-Id 绑定。服务跑在服务器上供远程访问时用这个。

那 SSE 算什么?它不是第三种传输,只是 HTTP 的一种响应形式(text/event-stream),服务端靠它把多条消息一条条推给客户端——可以理解成 Streamable HTTP 内部的一种机制。

规范上的细节(消息怎么分帧、响应怎么约定、会话怎么续传)见 MCP Transports 规范。

一张表记住怎么配

场景 服务端 客户端
本机进程(IDE 用) StdioTransport + Server.Run CommandTransport
远程 HTTP(推荐) NewStreamableHTTPHandler StreamableClientTransport
单测 / 同进程 InMemoryTransport(一对) 另一根
调试验证 包一层 LoggingTransport 同左
存量旧版 HTTP+SSE NewSSEHandler SSEClientTransport

本机进程:StdioTransport

服务端启动姿势,给 IDE 这类本地宿主用。

package main

import (
	"context"
	"log"

	"github.com/modelcontextprotocol/go-sdk/mcp"
)

func main() {
	s := mcp.NewServer(&mcp.Implementation{Name: "demo", Version: "1.0"}, nil)
	// … AddTool / AddResource 等
	if err := s.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
		log.Fatal(err)
	}
}

客户端拉起子进程:CommandTransport

客户端这边用 exec.Cmd 把上面那个进程拉起来,对端用的就是 StdioTransport。

c := mcp.NewClient(&mcp.Implementation{Name: "host", Version: "1.0"}, nil)
t := &mcp.CommandTransport{
	Command: exec.CommandContext(ctx, "./my-mcp-server"), // 或 go run 你的代码路径
}
sess, err := c.Connect(ctx, t, nil)
if err != nil {
	log.Fatal(err)
}
defer sess.Close()
// sess.CallTool / ListTools …

单测与同进程调试:InMemoryTransport

NewInMemoryTransports() 返回一对互通的 transport,连上之后完全走内存,不用真起进程和端口,写测试或演示很方便。注意顺序:先让 server 连,再连 client。

ts, tc := mcp.NewInMemoryTransports()

ss, err := server.Connect(ctx, ts, nil)
if err != nil {
	log.Fatal(err)
}
defer ss.Close()

cs, err := client.Connect(ctx, tc, nil)
if err != nil {
	log.Fatal(err)
}
defer cs.Close()

调试利器:LoggingTransport

把它包在任何 transport 外面,收发的消息会被抄一份到 io.Writer(调试时写 os.Stderr 就行),排查协议问题特别好用。用法就是在 NewClient 时把 Transport 字段换成包了一层的东西,一行代码的事。

跑成 HTTP 服务:StreamableHTTPHandler

服务端把 MCP server 包装成 http.Handler,注册进你自己的 HTTP 服务;客户端把 MCP endpoint 的 URL 填进 StreamableClientTransport。

// 服务端:包成 http.Handler
h := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server { return srv }, nil)
ts := httptest.NewServer(h)
defer ts.Close()

// 客户端:填 endpoint URL
t := &mcp.StreamableClientTransport{Endpoint: ts.URL}
sess, err := c.Connect(ctx, t, nil)

完整的服务端例子见跑成 HTTP 服务,注册到 gin 等框架的路由见挂到 gin 路由。

旧版 HTTP + SSE:SSEHandler / SSEClientTransport

这是 2024-11-05 那版规范(HTTP + SSE 双端点)的实现,接口和上面长得差不多:服务端用 NewSSEHandler,客户端 SSEClientTransport 填 SSE 根 URL(GET 开流,之后 POST 到 endpoint 事件给出的地址)。

现在只在新项目兼容存量服务时才用得上。有个坑值得记一下:SSEServerTransport 是服务端在处理某次 HTTP GET 时由 handler 创建的,所以它只能被 Connect,不能对它调 Run。

什么时候才需要 StreamableServerTransport

这个类型是给「自己处理 http.Request、自己按会话建 transport」的进阶场景用的。绝大多数项目用 NewStreamableHTTPHandler 就够,会话和路由它都封装好了,没必要手写这个类型。

Run 还是 Connect

启动 server 有两种姿势:Server.Run(ctx, transport) 和 Server.Connect(ctx, transport, opts)。

区别在于 Run 要求传输是「天生就绪、持久」的——StdioTransport 就是典型,进程一启动管道就在那儿了。HTTP 系的两个 handler 则是在收到入站请求时才现场创建一个 transport 并 Connect,会话跟着请求走,所以它们没有「Run」一说。

go-sdk 里 transport 相关的结构体一共九个(三个服务端套件拆开算),看着多,拆开就上面这几类,拿不准就照例子的姿势写。