Go SDK Design
This document discusses the design of a Go SDK for the model context protocol. The github.com/modelcontextprotocol/go-sdk/mcp package contains a prototype that we built to explore the MCP design space. Many of the ideas there are present in this document. However, we have diverged from and expanded on the APIs of that prototype, and this document should be considered canonical.
本文档探讨模型上下文协议(Model Context Protocol,MCP)Go SDK 的设计方案。github.com/modelcontextprotocol/go-sdk/mcp 包中包含一个原型实现prototype,我们通过该原型探索了 MCP 的设计空间。本文档中的许多理念均源自该原型,但我们已在原型 API 的基础上进行了调整与扩展,因此本文档应被视为权威参考。
canonical kəˈnɒnɪk(ə)l
kəˈnɑːnɪkl
根据教规的,按照宗教法规的;真经的,正经的;标准的,典范的;准确的,权威的;公认的,依据科学法则的;(数学表达式)最简洁的;(与)公理(或标准公式)(有关)的;(与)教会(或教士)(有关)的
diverge daɪˈvɜːdʒ
daɪˈvɜːrdʒ
相异,出现分歧;岔开,分开; 分化,偏离;使偏离,使分叉;(数)发散
Similarities and differences with mark3labs/mcp-go (and others)
与 mark3labs/mcp-go(及其他 SDK)的异同
The most popular unofficial MCP SDK for Go is mark3labs/mcp-go. As of this writing, it is imported by over 400 packages that span over 200 modules.
目前最受欢迎的非官方 MCP Go SDK 是 mark3labs/mcp-go。截至本文档撰写时( As of this writing),已有超过(span over) 200 个模块中的 400 多个包导入了该 SDK(it is imported by)
We admire mcp-go, and where possible tried to align with its design. However, the APIs here diverge in a number of ways in order to keep the official SDK minimal, allow for future spec evolution, and support additional features. We have noted significant differences from mcp-go in the sections below. Although the API here is not compatible with mcp-go, translating between them should be straightforward in most cases. (Later, we will provide a detailed translation guide.)
我们对 mcp-go 表示认可(admire),并在可能的情况下尽量与其设计保持一致(where possible tried to align with its design)。然而,为确保官方 SDK 的精简性、支持 MCP 规范的未来演进(evolution, 进化),以及实现更多扩展功能,本文档中的 API 在多个方面(in a number of ways)与 mcp-go 存在差异。下文会重点标注这些差异点。尽管本文档中的 API 与 mcp-go 不兼容,但多数情况下两者的转换逻辑较为简单(后续我们将提供详细的转换指南)。
evolution ˌiːvəˈluːʃ(ə)n
ˌevəˈluːʃ(ə)n
进化(论);演变,发展;(气体的)释放,(热量的)散发;队形变换,位置变换;<旧>开方
admire ədˈmaɪə(r)
ədˈmaɪər
钦佩,仰慕;欣赏,观赏
straightforward ˌstreɪtˈfɔːwəd
ˌstreɪtˈfɔːrwərd
简单的,易懂的;(人)诚实的,坦率的;直截了当地,坦率地
collaborate kəˈlæbəreɪt
kəˈlæbəreɪt
合作,协作;勾结,通敌
leverage ˈliːvərɪdʒ
ˈlevərɪdʒ
影响力,手段;杠杆力,杠杆作用;<美>杠杆比率
感谢所有为 mcp-go 及其他 Go 语言 MCP SDK 贡献代码的开发者。我们期待通过协作,将各方在 MCP 与 Go 语言结合方面的经验融入官方 SDK 中。
We hope that we can collaborate to leverage all that we've learned about MCP and Go in an official SDK.
我们希望我们可以在官方SDK中利用我们所学到的关于MCP和Go的所有知识。
Requirements 设计目标
These may be obvious, but it's worthwhile to define goals for an official MCP SDK. An official SDK should aim to be:
complete: it should be possible to implement every feature of the MCP spec, and these features should conform to all of the semantics described by the spec.
idiomatic: as much as possible, MCP features should be modeled using features of the Go language and its standard library. Additionally, the SDK should repeat idioms from similar domains.
robust: the SDK itself should be well tested and reliable, and should enable easy testability for its users.
future-proof: the SDK should allow for future evolution of the MCP spec, in such a way that we can (as much as possible) avoid incompatible changes to the SDK API.
extensible: to best serve the previous four concerns, the SDK should be minimal. However, it should admit extensibility using (for example) simple interfaces, middleware, or hooks.
以下目标虽看似显而易见,但仍有必要明确官方 MCP SDK 的核心定位:
完整性(Complete):能够实现 MCP 规范中的所有功能,且功能行为需完全符合/遵从(conform to)规范描述的语义(semantics)
符合语言习惯(Idiomatic):尽可能利用 Go 语言及其标准库的特性来建模 MCP 功能,同时借鉴同类领域(如网络、RPC)惯用设计模式
健壮性(Robust):SDK 自身需经过充分测试、确保可靠,同时应能让使用者轻松为基于 SDK 开发的代码编写测试testability
前瞻性(Future-Proof):支持 MCP 规范的未来演进,尽可能避免因规范更新导致 SDK API 出现不兼容变更
可扩展性(Extensible):为兼顾上述四点目标(concerns),SDK 需保持精简(minimal),但同时应支持通过简单接口、中间件/钩子(Hook)机制进行功能扩展(extensibility)
extensibility ɪksˌtensəˈbɪlɪti
ɪkˌstensəˈbɪlətɪ
展开性;可延长性
semantics / sɪˈmæntɪks /
/ sɪˈmæntɪks /
语义
idiomatic / ˌɪdiəˈmætɪk /
/ ˌɪdiəˈmætɪk /
(语言)自然地道的;与(某时期、人或团体的艺术或音乐风格)相合的,特点一致的
idioms / ˈɪdɪəm /
成语;惯用语;方言;风格(idiom 的复数)
testability / testəˈbɪləti /
/ testəˈbɪlɪti /
[计] 可测试性;易测性
admit ədˈmɪt
ædˈmɪt
(勉强)承认;招认,招供;准许进入(某处);接纳,接收(入学);收治,接收入院;承认......有效;容许,为......留有余地
Design
In the sections below, we visit each aspect of the MCP spec, in approximately the order they are presented by the official spec For each, we discuss considerations for the Go implementation, and propose a Go API.
下文将按照 MCP 官方规范的大致章节顺序,逐一探讨(For each)各部分功能在 Go 语言中的实现考量,并提出对应的 API 设计方案。
aspect / ˈæspekt /
/ ˈæspekt /
方面,特色;朝向,方位;外表,外观;(动词的)体
approximately / əˈprɒksɪmətli /
/ əˈprɑːksɪmətli /
大约,大概
Foundations - 基础架构
Package layout 包结构
In the sections that follow, it is assumed that most of the MCP API lives in a single shared package, the mcp package. This is inconsistent with other MCP SDKs, but is consistent with Go packages like net/http, net/rpc, or google.golang.org/grpc. We believe that having a single package aids(有助于) discoverability in package documentation and in the IDE. Furthermore, it avoids arbitrary decisions about package structure that may be rendered inaccurate by future evolution of the spec.
在后续(that follow)章节中,我们默认 MCP 的核心 API 集中在单个共享包 mcp 中(lives in a single shared package)。这一设计与其他 MCP SDK 不同,但与 Go 语言标准库中的 net/http、net/rpc 以及第三方库 google.golang.org/grpc 的包结构一致。我们认为单包设计有助于(aids)提升包文档和 IDE 中的功能可发现性,同时避免因规范未来演进导致包结构划分过时的问题。
discoverability / dɪˌskʌvərəˈbɪləti /
/ dɪˌskʌvərəˈbɪləti /
可暴露性、可发现性
aids / eɪdz /
/ eɪdz /
辅助物,辅助设施(aid 的复数);帮助,援助;促进,有助于(aid 的第三人称单数)
consistent / kənˈsɪstənt /
/ kənˈsɪstənt /
始终如一的,一贯的;持续的,连续的;固守的,坚持的;一致的,吻合的
furthermore / ˌfɜːðəˈmɔː(r) /
/ ˌfɜːrðərˈmɔːr /
此外,而且
arbitrary / ˈɑːbɪtrəri /
/ ˈɑːrbɪtreri /
任意的,随心所欲的;专横的,武断的
render / ˈrendə(r) /
/ ˈrendər /
使成为,使处于某种状态;给予,提供;(以某种方式)表达,表现;粉刷,往(墙上)抹灰;将(脂肪)熬成油,使熔化;正式宣布(判决或决定);(供审查,考虑)提交,呈报;<文>放弃,献出;演奏,演唱;翻译;绘制(指用色彩和明暗使轮廓图像具立体感);秘密引渡(外国罪犯、恐怖嫌疑犯);从(动物身体)提取(蛋白质、脂肪及其他可用部分)
Functionality that is not directly related to MCP (like jsonschema or jsonrpc2) belongs in a separate package
与 MCP 无直接关联的功能(如 JSON Schema 解析、JSON-RPC 2.0 实现)应放在(belongs in)独立包中
Therefore, this is the core package layout, assuming github.com/modelcontextprotocol/go-sdk as the module path.
github.com/modelcontextprotocol/go-sdk/mcp: the bulk of the user facing API
github.com/modelcontextprotocol/go-sdk/jsonschema: a jsonschema implementation, with validation
github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2: a fork of x/tools/internal/jsonrpc2_v2
The JSON-RPC implementation is hidden, to avoid tight coupling. As described in the next section, the only aspects of JSON-RPC that need to be exposed in the SDK are the message types, for the purposes of defining custom transports. We can expose these types by promoting them from the mcp package using aliases or wrappers.
基于以上原则,假设模块路径为 github.com/modelcontextprotocol/go-sdk,核心包结构如下:
github.com/modelcontextprotocol/go-sdk/mcp:包含大部分面向用户的 API(the bulk of the user facing API)
github.com/modelcontextprotocol/go-sdk/jsonschema:JSON Schema 实现,包含校验功能
github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2:基于 x/tools/internal/jsonrpc2_v2 的分支版本(内部使用,不对外暴露)
JSON-RPC 实现被设计为内部依赖,以避免强耦合(tight coupling)。如下文所述,SDK 只需对外暴露 JSON-RPC 的消息类型(用于自定义传输层),可通过 mcp 包的类型别名或包装类实现暴露。
与 mcp-go 的差异:本文档中的 mcp 包整合了 mcp-go 中 mcp、client、server 和 transport 四个包的所有功能。
tight coupling / taɪt ˈkʌplɪŋ /
紧耦合:在电脑运算和系统设计中,紧耦合的系统是指两个或多个组件之间的依赖关系非常强,彼此之间紧密联系,一旦其中一个组件发生变化,可能会对其他组件产生较大的影响。
Difference from mcp-go: Our mcp package includes all the functionality of mcp-go's mcp, client, server and transport packages.
JSON-RPC and Transports
The MCP is defined in terms of client-server communication over bidirectional JSON-RPC message connections. Specifically, version 2025-03-26 of the spec defines two transports:
MCP 规范定义了客户端与服务端通过双向 JSON-RPC 消息连接进行通信。具体而言:
2025-03-26 版本规范定义了两种传输层:
标准输入输出(stdio):通过子进程(subprocess)的标准输入(stdin)/ 标准输出(stdout)通信
可流式 HTTP(streamable http):通过一系列复杂的 text/event-stream GET 请求和 HTTP POST 请求通信
stdio: communication with a subprocess over stdin/stdout
streamable http: communication over a relatively complicated series of text/event-stream GET and HTTP POST requests
Additionally, version 2024-11-05 of the spec defined a simpler (yet stateful) HTTP transport:
2024-11-05 版本规范还定义了一种更简单的(但有状态的 stateful)HTTP 传输层:
sse: client issues a hanging GET request and receives messages via text/event-stream, and sends messages via POST to a session endpoint.
服务器发送事件(SSE):客户端通过 "挂起的(hanging) GET 请求"(hanging GET)接收 text/event-stream 消息,通过 POST 请求向会话端点发送消息。
Furthermore, the spec states that it must be possible for users to define their own custom transports.
此外,规范明确要求支持用户自定义传输层
Given the diversity of the transport implementations, they can be challenging to abstract. However, since JSON-RPC requires a bidirectional connection, we can use this to model the MCP transport abstraction:
尽管(Given)传输层实现多样且难以抽象(be challenging to abstract),但由于 JSON-RPC 本质上需要双向连接,我们可基于这一特性定义 MCP 传输层的抽象接口:
go
type (
JSONRPCID = jsonrpc2.ID // JSON-RPC 请求 ID 类型别名
JSONRPCMessage = jsonrpc2.Message // JSON-RPC 消息类型别名
JSONRPCRequest = jsonrpc2.Request // JSON-RPC 请求类型别名
JSONRPCResponse = jsonrpc2.Response // JSON-RPC 响应类型别名
)
// Transport 用于创建 MCP 客户端与服务端之间的双向连接
type Transport interface {
Connect(ctx context.Context) (Connection, error)
}
// Connection 表示双向 JSON-RPC 连接
type Connection interface {
Read(ctx context.Context) (JSONRPCMessage, error) // 读取 JSON-RPC 消息
Write(ctx context.Context, JSONRPCMessage) error // 写入 JSON-RPC 消息
Close() error // 关闭连接(符合 io.Closer 接口)
}
接口方法均接收 context.Context 并返回 error,符合 Go 语言中 I/O 操作的惯用 API 设计
Methods accept a Go Context and return an error, as is idiomatic(符合语言习惯,符合的) for APIs that do I/O.
A Transport is something that connects a logical JSON-RPC connection, and nothing more. Connections must be closeable in order to implement client and server shutdown, and therefore conform to the io.Closer interface
Transport 的职责仅为 "建立逻辑上的(logical) JSON-RPC 连接",不包含额外功能。Connection 必须支持关闭操作(closeable),以实现客户端和服务端的优雅 shutdown,因此需实现(conform to) io.Closer 接口
Other SDKs define higher-level transports, with, for example, methods to send a notification or make a call. Those are jsonrpc2 operations on top of the logical connection, and the lower-level interface is easier to implement in most cases, which means it is easier to implement custom transports.
其他 MCP SDK 会定义更高层级(higher-level)的传输层(例如提供发送通知、发起调用的方法),但这些功能本质上是基于逻辑连接的(on top of the logical connection) JSON-RPC 操作。本文档中的低层级接口更易于实现,从而降低自定义传输层的开发成本。
For our prototype, we've used an internal jsonrpc2 package based on the Go language server gopls, which we propose to fork for the MCP SDK. It already handles concerns like client/server connection, request lifecycle, cancellation, and shutdown.
在原型实现中,我们使用了基于 Go 语言服务器 gopls 的内部 jsonrpc2 包,建议将该包分支后集成到 MCP SDK 中
该包已处理客户端 / 服务端连接、请求生命周期、取消操作、shutdown 等核心逻辑
propose / prəˈpəʊz /
/ prəˈpoʊz /
提议,建议;提出(理论或解释);提名,推荐;计划,打算;求婚;(向立法机构或委员会)提交(动议);提议祝(酒)
differences / ˈdɪfərənsɪz /
/ ˈdɪfərənsɪz /
偏差,差异(difference 复数形式)
Differences from mcp-go: The Go team has a battle-tested JSON-RPC implementation that we use for gopls, our Go LSP server. We are using the new version of this library as part of our MCP SDK. It handles all JSON-RPC 2.0 features, including cancellation.
与 mcp-go 的差异:Go 团队为 gopls(Go 语言 LSP 服务器)开发了经过实战验证(battle-tested)的 JSON-RPC 实现,本文档中的 SDK 采用该库的新版本。它支持 JSON-RPC 2.0 的所有特性,包括请求取消。
The Transport interface here is lower-level than that of mcp-go, but serves a similar purpose. We believe the lower-level interface is easier to implement.
本文档中的 Transport 接口比 mcp-go 的传输层接口更底层,但用途相似。我们认为低层级接口更易于实现
stdio transports 标准输入输出传输层
In the MCP Spec, the stdio transport uses newline-delimited JSON to communicate over stdin/stdout. It's possible to model both client side and server side of this communication with a shared type that communicates over an io.ReadWriteCloser. However, for the purposes of future-proofing, we should use a different types for client and server stdio transport.
根据 MCP 规范,stdio 传输层通过 "换行分隔的 JSON"(newline-delimited JSON)在 stdin/stdout 上通信。理论上,可通过一个共享类型(基于 io.ReadWriteCloser)同时实现客户端和服务端的 stdio 通信,但为保证前瞻性(future-proofing),建议为客户端和服务端分别定义不同的 stdio 传输层类型。
delimited / diːˈlɪmɪtɪd /
划定......的界限;限定(delimit 的过去分词);划定界限的;被限定了的
proofing / ˈpruːfɪŋ /
/ ˈpruːfɪŋ /
The CommandTransport is the client side of the stdio transport, and connects by starting a command and streaming JSON-RPC messages over stdin/stdout.
stdio transport 的 客户端侧:CommandTransport,通过启动外部命令,在子进程的 stdin/stdout 上传输 JSON-RPC 消息
go
// CommandTransport 是一种 Transport,通过启动外部命令,在其 stdin/stdout 上以"换行分隔的 JSON"通信
type CommandTransport struct { Command *exec.Command }
// Connect 启动命令并通过 stdin/stdout 建立连接
func (*CommandTransport) Connect(ctx context.Context) (Connection, error)
The StdioTransport is the server side of the stdio transport, and connects by binding to os.Stdin and os.Stdout
stdio transport 的 服务端侧:StdioTransport,通过绑定 os.Stdin 和 os.Stdout 通信
go
// A StdioTransport is a [Transport] that communicates using newline-delimited
// JSON over stdin/stdout.
type StdioTransport struct { }
func (t *StdioTransport) Connect(context.Context) (Connection, error)
HTTP transports
The HTTP transport APIs are even more asymmetrical. Since connections are initiated via HTTP requests, the client developer will create a transport, but the server developer will typically install an HTTP handler. Internally, the HTTP handler will create a logical transport for each new client connection.
HTTP 传输层的客户端与服务端 API 差异更大:客户端通过创建传输层发起连接,而服务端通常通过注册 HTTP 处理器(Handler)接收连接。在内部实现中,HTTP 处理器会为每个新客户端连接创建一个逻辑传输层。
asymmetrical / ˌeɪsɪˈmetrɪk(ə)l /
/ ˌeɪsɪˈmetrɪk(ə)l /
非对称的;不匀称的,不对等的
Importantly, since they serve many connections, the HTTP handlers must accept a callback to get an MCP server for each new session. As described below, MCP servers can optionally connect to multiple clients. This allows customization of per-session servers: if the MCP server is stateless, the user can return the same MCP server for each connection. On the other hand, if any per-session customization is required, it is possible by returning a different Server instance for each connection.
重要的是,由于 HTTP 处理器需处理多个连接,必须支持通过 "回调函数" 为每个新会话绑定对应的 MCP 服务端。如下文所述,MCP 服务端可选择支持多个客户端连接,这使得 "按会话自定义服务端" 成为可能:若服务端无状态,用户可对所有连接返回同一个服务端实例;若需按会话定制(如绑定用户身份),则可为每个连接返回不同的 Server 实例。
Both the SSE and Streamable HTTP server transports are http.Handlers which serve messages to their associated connection. Consequently, they can be connected at most once.
SSE 和可流式 HTTP 服务端传输层均为 http.Handler,每个处理器仅能绑定一个连接(即每个处理器对应一个会话)。
go
// SSEHTTPHandler is an http.Handler that serves SSE-based MCP sessions as defined by
// the 2024-11-05 version of the MCP protocol.
// SSEHTTPHandler 是一个 http.Handler,实现 MCP 规范 2024-11-05 版本定义的 SSE 会话
type SSEHTTPHandler struct { /* unexported fields */ }
// NewSSEHTTPHandler returns a new [SSEHTTPHandler] that is ready to serve HTTP.
//
// The getServer function is used to bind created servers for new sessions. It
// is OK for getServer to return the same server multiple times.
// NewSSEHTTPHandler 创建一个可处理 HTTP 请求的 SSEHTTPHandler
// getServer 函数用于为新会话绑定服务端,允许返回同一个服务端实例
func NewSSEHTTPHandler(getServer func(request *http.Request) *Server) *SSEHTTPHandler
// ServeHTTP 处理 HTTP 请求(实现 http.Handler 接口)
func (*SSEHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
// Close prevents the SSEHTTPHandler from accepting new sessions, closes active
// sessions, and awaits their graceful termination.
// Close 阻止处理器接收新会话、关闭活跃会话,并等待其优雅终止
func (*SSEHTTPHandler) Close() error
Notably absent are options to hook into low-level request handling for the purposes of authentication or context injection. These concerns are instead handled using standard HTTP middleware patterns. For middleware at the level of the MCP protocol, see Middleware below.
需注意,SDK 未提供 "钩子函数" (Notably absent are options to hook)用于拦截底层请求(如鉴权、上下文注入),这类需求应通过标准 HTTP 中间件实现。关于 MCP 协议层级的中间件,详见下文 "中间件(Middleware)" 部分。
Notably absent 明显缺席,明显不存在
authentication / ɔːˌθentɪˈkeɪʃn /
/ ɔːˌθentɪˈkeɪʃn /
证明真实性,鉴定;身份验证,认证
instances / ˈɪnstənsɪz /
sends / sendz /
/ sendz /
"dz" 是英语中典型的 "浊辅音组合":
字母 "d" 是浊辅音 /d/,"z" 是浊辅音 /z/,二者连读时会融合成一个 "浊辅音簇" /dz/(发音类似中文 "兹" 的尾音,但更轻、更短,且带有 "d" 的轻微顿挫感)
By default, the SSE handler creates messages endpoints with the ?sessionId=...
query parameter. Users that want more control over the management of sessions and session endpoints may write their own handler, and create SSEServerTransport instances themselves for incoming GET requests.
默认情况下,SSE 处理器会创建带有 ?sessionId=...
查询参数的消息端点。若用户需自定义会话管理或端点路径,可自行实现处理器(write their own handler),并为每个传入的 GET 请求创建 SSEServerTransport 实例
go
// A SSEServerTransport is a logical SSE session created through a hanging GET
// request.
// SSEServerTransport 是通过"挂起的 GET 请求"创建的逻辑 SSE 会话
type SSEServerTransport struct {
Endpoint string // 会话端点
Response http.ResponseWriter // 用于发送 SSE 消息的响应对象
}
// ServeHTTP handles POST requests to the transport endpoint.
// ServeHTTP 处理该传输层端点的 POST 请求
func (*SSEServerTransport) ServeHTTP(w http.ResponseWriter, req *http.Request)
// Connect sends the 'endpoint' event to the client.
// See [SSEServerTransport] for more details on the [Connection] implementation.
// Connect 向客户端发送 'endpoint' 事件,详见 Connection 实现说明
func (*SSEServerTransport) Connect(context.Context) (Connection, error)
The SSE client transport is simpler, and hopefully self-explanatory.
SSE 客户端传输层实现更简洁,逻辑自明(self-explanatory)
explanatory / ɪkˈsplænət(ə)ri /
/ ɪkˈsplænətɔːri /
解释的,说明的
go
type SSEClientTransport struct {
// Endpoint is the SSE endpoint to connect to.
// SSE 连接端点(URL)
Endpoint string
// HTTPClient is the client to use for making HTTP requests. If nil,
// http.DefaultClient is used.
// 用于发送 HTTP 请求的客户端,为 nil 时使用 http.DefaultClient
HTTPClient *http.Client
}
// Connect connects through the client endpoint.
// Connect 通过客户端端点建立连接
func (*SSEClientTransport) Connect(ctx context.Context) (Connection, error)
The Streamable HTTP transports are similar to the SSE transport, albeit with a more complicated implementation. For brevity, we summarize only the differences from the equivalent SSE types:
可流式 HTTP 传输层与 SSE 传输层类似,但实现更复杂。以下仅列出与 SSE 类型的差异
albeit / ˌɔːlˈbiːɪt /
/ ˌɔːlˈbiːɪt /
虽然,尽管
differences / ˈdɪfərənsɪz /
/ ˈdɪfərənsɪz /
偏差,差异(difference 复数形式)
brevity / ˈbrevəti /
/ ˈbrevəti /
简洁,简短;短暂,短促
symmetrical / sɪˈmetrɪk(ə)l /
/ sɪˈmetrɪk(ə)l /
对称的,匀称的
delegate / ˈdelɪɡət /
/ ˈdelɪɡət /
代表;委员会成员;授权,把......委托给;选派(某人做某事)
resumption / rɪˈzʌmpʃn /
/ rɪˈzʌmpʃ(ə)n /
<正式>重新开始,恢复;<法律>(土地、权利等的)重获
go
// The StreamableHTTPHandler interface is symmetrical to the SSEHTTPHandler.
// StreamableHTTPHandler 接口与 SSEHTTPHandler 对称
type StreamableHTTPHandler struct { /* unexported fields */ }
// NewStreamableHTTPHandler 创建可处理 HTTP 请求的 StreamableHTTPHandler
func NewStreamableHTTPHandler(getServer func(request *http.Request) *Server) *StreamableHTTPHandler
func (*StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
func (*StreamableHTTPHandler) Close() error
// Unlike the SSE transport, the streamable transport constructor accepts a
// session ID, not an endpoint, along with the HTTP response for the request
// that created the session. It is the caller's responsibility to delegate
// requests to this session.
// 与 SSE 传输层不同,可流式传输层的构造函数需传入会话 ID(而非端点)和创建会话的请求响应
// 调用者需负责将请求路由到对应会话
type StreamableServerTransport struct {
// SessionID is the ID of this session.
// 会话 ID
SessionID string
// Storage for events, to enable stream resumption.
// 事件存储,用于支持流恢复(stream resumption)
EventStore EventStore
}
func (*StreamableServerTransport) ServeHTTP(w http.ResponseWriter, req *http.Request)
func (*StreamableServerTransport) Connect(context.Context) (Connection, error)
go
// The streamable client handles reconnection transparently to the user.
// 可流式客户端传输层自动处理重连逻辑,对用户透明
type StreamableClientTransport struct {
Endpoint string // 连接端点
HTTPClient *http.Client // HTTP 客户端
ReconnectOptions *StreamableReconnectOptions // 重连配置
}
func (*StreamableClientTransport) Connect(context.Context) (Connection, error)
populate / ˈpɒpjuleɪt /
/ ˈpɑːpjuleɪt /
(大批人或动物)居住于,生活于;充满,出现于(某地方,领域);迁移,殖民于;(给文件)增添数据,输入数据
overlay / ˌəʊvəˈleɪ /
/ ˌoʊvərˈleɪ /
覆在......上面(overlie 的过去式);覆盖,铺;(尤指以感情或品质)撒满;(声音)盖过
套图透明膜;覆盖图;覆盖物,涂层;覆盖;附加的特性
Differences from mcp-go: In mcp-go, server authors create an MCPServer, populate it with tools, resources and so on, and then wrap it in an SSEServer or StdioServer. Users can manage their own sessions with RegisterSession and UnregisterSession. Rather than use a server constructor to get a distinct server for each connection, there is a concept of a "session tool" that overlays tools for a specific session.
与 mcp-go 的差异:在 mcp-go 中,服务端开发者需创建 MCPServer、注册工具 / 资源,再将其包装为 SSEServer 或 StdioServer,并通过 RegisterSession/UnregisterSession 管理会话。mcp-go 不支持 "按连接创建服务端(get a distinct server for each connection)",而是通过 "会话工具(session tool)" 为特定会话叠加工具(overlays tools for a specific session)。
Here, we tried to differentiate the concept of a Server, HTTPHandler, and Transport, and provide per-session customization through either the getServer constructor or middleware. Additionally, individual handlers and transports here have a minimal API, and do not expose internal details. (Open question: are we oversimplifying?)
本文档中,我们明确区分了 Server、HTTPHandler 和 Transport 的概念,并通过 getServer 构造函数或中间件支持按会话定制服务端(per-session customization)。此外,本文档中的处理器和传输层 API 更精简,不暴露内部实现细节。(待讨论:是否过度简化?)
Other transports
We also provide a couple of transport implementations for special scenarios. An InMemoryTransport can be used when the client and server reside in the same process. A LoggingTransport is a middleware layer that logs RPC logs to a desired location, specified as an io.Writer.
couple / ˈkʌp(ə)l /
/ ˈkʌp(ə)l /
两个,几个;一对夫妇,一对情侣
加上,结合;(把设备等)连接;形成一双,配成一对;<旧>交配,交媾;(用电磁感应,静电荷或光学通信线路)将(电路元件)耦合
reside / rɪˈzaɪd /
/ rɪˈzaɪd /
<正式>居住,定居;<正式>存在于;<正式>(权利、权力等)属于;位(于)
SDK 还提供两种特殊场景下的传输层实现:
内存传输层(InMemoryTransport):适用于客户端与服务端在同一进程内的场景
日志传输层(LoggingTransport):中间件层级的传输层,将 RPC 日志写入指定 io.Writer
go
// InMemoryTransport 是一种通过内存网络连接通信的 Transport,使用"换行分隔的 JSON"
type InMemoryTransport struct { /* ... */ }
// NewInMemoryTransports 返回两个相互连接的 InMemoryTransport 实例
func NewInMemoryTransports() (*InMemoryTransport, *InMemoryTransport)
// LoggingTransport 是一种代理传输层,将 RPC 日志写入 Writer,核心功能委托给 Delegate
type LoggingTransport struct {
Delegate Transport // 被代理的核心传输层
Writer io.Writer // 日志输出目标
}
附录
挂起的 GET 请求
在 SSE(Server-Sent Events)中,挂起的 GET 请求是指客户端通过EventSource API 向服务器发送一个 GET 请求后,服务器并不立即返回响应,而是保持该请求处于挂起状态
服务器保持这个请求挂起,直到有新的数据需要推送给客户端时,才会向客户端返回数据
在等待数据的过程中,连接不会关闭,服务器可以随时通过这个挂起的请求将数据推送给客户端
如果一直没有新数据,连接会保持到超时时间,然后客户端可以选择重新发起请求
这种挂起的 GET 请求机制是 SSE 实现服务器向客户端单向实时推送数据的关键
它与长轮询类似,但不同之处在于 SSE 在一次连接中可以处理多个消息
长轮询通常在服务端返回数据后就会关闭连接,客户端需要再次发起请求
SSE 是一种服务器向客户端单向推送数据的技术,基于 HTTP 长连接。
客户端通过创建 EventSource 对象并发送 GET 请求来建立与服务器的连接,服务器接收到请求后,会保持这个连接打开,并通过该连接持续向客户端推送数据,直到连接被主动关闭或发生超时。
SSE 使用挂起的 GET 请求,是因为这种方式可以利用 HTTP 协议的特性,实现简单且高效的服务器推送功能,避免了频繁建立和关闭连接带来的开销。
POST 请求通常用于向服务器提交数据,比如在表单提交等场景中。SSE 的设计目的是单向数据推送,不需要客户端向服务器发送大量数据,因此不需要使用 POST 请求。并且,SSE 规范中明确要求客户端必须使用 GET 请求,标准的 EventSource API 不支持 POST 请求。
不过,在一些特殊场景下,可能会出现同时涉及 GET 和 POST 请求的情况。例如,客户端可能先通过 POST 请求将一些参数或数据发送到服务器,服务器处理完这些数据后,再通过 SSE 的 GET 请求连接向客户端推送处理结果或实时更新信息。
但这并不是 SSE 本身的机制要求,而是根据具体业务需求进行的组合使用。
NDJSON
什么是 Newline-Delimited JSON(NDJSON)
Newline-Delimited JSON(简称 NDJSON,也常被称为 Line-Delimited JSON 或 LDJSON)是一种轻量级的 JSON 数据传输 / 存储格式
核心规则是 "每行一个独立的 JSON 对象"------ 用换行符(\n
,Unix 风格)分隔多个 JSON 数据
确保每个换行符之间的内容都是语法完整的 JSON(如对象、数组、字符串等,但最常见的是 JSON 对象)
它不追求像标准 JSON 数组那样用 [ ] 包裹所有元素、用逗号分隔的 "整体性"
通过换行符实现 "逐个分隔",本质是多个独立 JSON 数据的 "行级集合"
NDJSON 的核心格式规则
NDJSON 的规则非常简单,仅需遵守两点:
单行完整性:每一行必须是一个独立、语法正确的 JSON 数据(不能将一个 JSON 对象拆分成多行,也不能在一行中放多个 JSON 对象)
换行分隔:多个 JSON 数据之间用单个换行符(\n
) 分隔(不允许用 \r\n
等其他换行格式,避免跨平台解析问题)
可选的尾换行:文件或数据流的末尾可以有一个可选的换行符(不强制,但建议保留,避免最后一行解析不完整)
NDJSON 与标准 JSON 的对比
特性 标准 JSON(数组形式) NDJSON(行分隔形式)
语法依赖 必须用 [ ] 包裹所有元素,元素间用逗号分隔(逗号不能多 / 少) 无需包裹符,仅靠换行分隔,无逗号依赖
解析方式 必须读取完整的数据流才能解析(否则无法判断数组是否结束) 可逐行解析,读取一行就解析一个 JSON 对象
适用场景 数据量小、需一次性处理的场景(如配置文件、API 单次响应) 大数据流、实时传输场景(如日志、批量数据同步)
NDJSON 的核心优势:为什么需要它?
NDJSON 诞生的核心目的是解决 "大体积 JSON 数据的解析效率" 和 "流式数据的实时处理" 问题,相比标准 JSON 有两个关键优势:
支持 "流式解析",降低内存占用
标准 JSON 数组必须读取完整的数据流(直到闭合 ]
)才能开始解析 ------ 如果数据量极大(如 1GB 的用户日志),一次性加载到内存会导致内存溢出
NDJSON 可以逐行读取、逐行解析:读取一行就解析一个 JSON 对象,处理完后释放该行内存,再读取下一行。即使数据量达到 GB 级,内存占用也能保持在较低水平(仅需存储当前行的内容)
在处理日志时,服务器可以实时将每条日志以 NDJSON 行的形式写入文件
客户端读取时无需等待文件写完,读一行解析一行,实现 "实时日志分析"
容错性更高,部分错误不影响整体
标准 JSON 数组中只要有一个元素语法错误(如少逗号、引号不闭合),整个数组都会解析失败
而 NDJSON 中每行是独立的 ------ 即使某一行 JSON 语法错误,仅该行解析失败,其他行仍能正常处理
批量同步 1000 条用户数据时,若第 500 条数据格式错误,NDJSON 可以跳过错误行继续处理后续 500 条,而标准 JSON 会整体失败
易于生成和手动编辑
NDJSON 无需处理 "数组闭合符" 和 "元素逗号"------ 生成数据时,只需每次输出一个 JSON 对象加换行符即可,无需判断 "是否是最后一个元素"(避免标准 JSON 中常见的 "尾逗号错误")
逐行结构也方便手动编辑(如用记事本修改某条数据,无需担心破坏数组结构)
NDJSON 的典型应用场景
NDJSON 因 "流式处理" 和 "高容错" 特性,广泛用于以下场景:
日志存储分析:如应用日志、服务器访问日志,每条日志是一个 JSON 对象,按行存储(方便 ELK、Fluentd 等日志系统逐行采集解析)
批量数据传输:如数据库备份(每行一条记录)、API 批量响应(如分页返回时,用 NDJSON 逐行返回数据,避免拼接大数组)
实时数据推送:如 SSE(Server-Sent Events)或 stdio 传输(如你之前接触的 MCP 协议 stdio 传输层),服务器通过 NDJSON 逐行向客户端推送实时数据(如状态更新、消息通知)
大数据处理:如 Hadoop、Spark 等分布式计算框架,处理海量 JSON 数据时,用 NDJSON 逐行读取,降低内存压力
NDJSON 的解析与生成(以 Go 为例)
NDJSON 的处理逻辑非常简单,几乎所有编程语言都能通过 "逐行读写 + JSON 解析" 实现,无需依赖特殊库
生成 NDJSON(将多条数据按行输出)
go
package main
import (
"encoding/json"
"fmt"
"os"
)
// 定义数据结构
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
// 模拟数据
users := []User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
{ID: 3, Name: "Charlie"},
}
// 逐行生成 NDJSON 并写入标准输出
for _, u := range users {
// 将单个 User 序列化为 JSON 字节
data, err := json.Marshal(u)
if err != nil {
fmt.Fprintf(os.Stderr, "marshal error: %v\n", err)
continue
}
// 输出 JSON 字节 + 换行符
fmt.Println(string(data))
}
}
运行后输出(标准 NDJSON 格式):
{"id":1,"name":"Alice"}
{"id":2,"name":"Bob"}
{"id":3,"name":"Charlie"}
解析 NDJSON(逐行读取并解析)
go
package main
import (
"bufio"
"encoding/json"
"fmt"
"os"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
// 打开 NDJSON 文件(或从标准输入读取)
file, err := os.Open("users.ndjson")
if err != nil {
fmt.Fprintf(os.Stderr, "open file error: %v\n", err)
return
}
defer file.Close()
// 用 bufio.Scanner 逐行读取
scanner := bufio.NewScanner(file)
lineNum := 0
for scanner.Scan() {
lineNum++
line := scanner.Bytes() // 当前行的字节(避免字符串拷贝)
// 解析当前行的 JSON
var u User
if err := json.Unmarshal(line, &u); err != nil {
fmt.Fprintf(os.Stderr, "line %d parse error: %v\n", lineNum, err)
continue
}
// 处理解析后的数据
fmt.Printf("Parsed User: ID=%d, Name=%s\n", u.ID, u.Name)
}
// 检查读取过程中的错误(如文件损坏)
if err := scanner.Err(); err != nil {
fmt.Fprintf(os.Stderr, "scan file error: %v\n", err)
}
}
运行后输出(解析结果):
go
Parsed User: ID=1, Name=Alice
Parsed User: ID=2, Name=Bob
Parsed User: ID=3, Name=Charlie
注意事项
换行符规范:必须使用 Unix 风格的 \n
(换行),不能用 Windows 风格的 \r\n
(回车 + 换行)------ 若数据来自 Windows 系统,需先替换换行符,否则解析时会把 \r
当作无效字符导致错误
单行 JSON 完整性:每行必须是完整的 JSON(如不能将 {"id":1,"name":"Alice"}
拆成两行),否则会触发 JSON 解析错误
不支持注释:与标准 JSON 一致,NDJSON 不允许在每行中添加注释(//
或 /* */
),注释会导致 JSON 语法错误
数据类型限制:每行 JSON 可以是任意合法 JSON 类型(如字符串、数字、数组),但实际应用中最常见的是 JSON 对象(键值对结构),因为对象更适合存储结构化数据(如日志、用户信息)
总结
总结来说,NDJSON 是一种 "为效率而生" 的 JSON 变体 ------ 它放弃了标准 JSON 的 "整体结构",换取了 "流式处理" 和 "高容错" 能力,特别适合大数据量、实时性要求高的场景,也是你之前接触的 MCP 协议 stdio 传输层的核心数据格式(通过 NDJSON 逐行传输 JSON-RPC 消息)。