API 設計的 “Go 境界”:Go 團隊設計 MCP SDK 過程中的取捨與思考

大家好,我是 Tony Bai。

作爲開發者,我們每天都在與 API 打交道——調用它們,設計它們,有時也會爲糟糕的 API 設計而頭痛不已。一個優秀的 API,如同一位技藝精湛的嚮導,能清晰、高效地引領我們通往復雜功能的彼岸;而一個蹩腳的 API,則可能像一座佈滿陷阱的迷宮,讓我們步履維艱。

那麼,在 Go 語言的世界裏,一個 “好” 的 API 應該是什麼樣子的?它應該如何體現 Go 語言簡潔、高效、併發安全的哲學?它又如何在滿足功能需求的同時,保持對開發者的友好和對未來的兼容?

最近,Go 官方團隊爲 Model Context Protocol (MCP) 發起了一項 Go SDK 的設計討論,並公開了其詳細的設計草案以及一個初期的原型代碼實現。這份設計稿與代碼,在我看來,不僅僅是對 MCP 協議的 Go 語言實現規劃,更是一份 Go 官方團隊關於 API 設計思考與實踐的 “公開課”。它向我們生動地展示了,在打造一個既強大又符合 Go 慣例 (Idiomatic Go) 的 SDK 時,需要在哪些維度進行權衡取捨,以及如何將 Go 的設計哲學融入到每一個細節之中。

今天,就讓我們一同走進這份設計稿和它的原型代碼,探尋 Go 團隊在 API 設計中所追求的 “Go 境界”。

API 設計的 “初心”:Go 團隊爲 MCP SDK 設定的目標

在深入細節之前,我們先來看看 Go 團隊爲這個官方 MCP SDK 設定了哪些核心目標 (Requirements)。這些目標,本身就是設計任何高質量 Go SDK 的重要準則:

  1. 完整性 (Complete): 能夠實現 MCP 規範中的所有特性,並嚴格遵循其語義。這是 SDK 作爲協議實現的基本要求。

  2. 符合 Go 慣例 (Idiomatic): 這是 “Go 境界” 的核心。SDK 應最大限度地利用 Go 語言自身的特性和標準庫的設計風格,並重復 Go 生態中相似領域(如 net/httpgrpc-go)已形成的習慣用法。

  3. 健壯性 (Robust): SDK 自身必須是經過良好測試、穩定可靠的,並且要能讓使用者輕鬆地對他們基於 SDK 構建的應用進行測試。

  4. 面向未來 (Future-proof): 設計必須考慮到 MCP 規範未來可能的演進,儘可能地避免因規範變更而導致 SDK API 發生不兼容的破壞性改動。

  5. 可擴展性 (Extensible) 與最小化 (Minimal): 爲了最好地服務於前述四個目標,SDK 的核心 API 應保持最小化、正交化。同時,它必須允許用戶通過簡單、清晰的方式(如接口、中間件、鉤子等)進行擴展,以滿足特定需求。

這些目標清晰地勾勒出了 Go 團隊對一個 “好” 的 Go SDK 的期望:它不僅要功能完備,更要“寫起來像 Go,用起來像 Go”,並且能經受住時間的考驗。

庖丁解牛:MCP Go SDK 設計中的 “Go 味” 與權衡

設定了清晰的 API 設計目標後,Go 團隊便開始將這些原則付諸實踐,着手設計 MCP Go SDK 的具體結構與接口。細細品讀這份設計稿和其原型代碼,我們能從多個關鍵的決策中,清晰地品味出濃濃的 “Go 味”,並深刻體會到他們在功能完備性、語言慣例、當前易用性與未來演進性之間所做的精妙權衡。

包佈局

在 SDK 的整體結構上,Go 團隊針對包的佈局做出了一個顯著的選擇,這直接體現了他們對 Go 生態習慣的深刻理解和對開發者體驗的優先考量。不同於其他語言的 MCP SDK 可能會將客戶端、服務端、傳輸層等功能細緻地拆分到各自獨立的包中,Go 團隊提議將 SDK 的核心用戶接口集中在單個 mcp 包內

這種做法與 Go 標準庫中的 net/httpnet/rpc 以及社區廣泛採納的 google.golang.org/grpc 等核心包的組織方式保持了高度一致。對於 Go 開發者而言,這意味着更低的認知門檻——當他們需要使用 MCP 功能時,幾乎所有的核心 API 都能在同一個 mcp 包下找到,這極大地提升了 API 的發現性。同時,集中的包結構也更利於生成聚合的包文檔,並在 IDE 中提供更流暢的代碼提示與導航體驗。

更深一層的考量,則是爲了 SDK 的長期穩定性和麪向未來的適應性。如果將功能過度拆分到多個細粒度的包中,未來 MCP 規範的任何微小調整,都可能引發連鎖的包結構變動或複雜的跨包依賴問題。而單一核心包的設計,則能更好地吸收這些變化,減少對用戶代碼的衝擊。當然,像 JSON Schema 這種與 MCP 核心邏輯不直接相關、但又可能被 SDK 用戶需要的輔助功能,則被合理地規劃到了獨立的子包(如 jsonschema/)中,做到了關注點分離。雖然這種策略可能會讓一些追求極致 “模塊化” 的開發者覺得核心包略顯“龐大”,但 Go 團隊在此顯然是權衡了用戶發現性、文檔清晰度以及長期演進的穩定性,將它們放在了更高的優先級。

JSON-RPC 與傳輸層抽象 (Transports)

MCP 協議的核心在於通過 JSON-RPC 在客戶端和服務端之間交換消息,而其底層可以有多種傳輸方式,如 stdio、可流式 HTTP、SSE 等。如何爲這些形態各異的傳輸方式設計一個統一且靈活的抽象層,是對 SDK 設計者的一大考驗。Go 團隊在這裏再次展現了其對接口設計藝術的嫺熟運用。

在 transport.go 中,他們定義了一個非常底層的 Transport 接口:

// A Transport is used to create a bidirectional connection between MCP client
// and server.
type Transport interface {
 Connect(ctx context.Context) (Stream, error)
}

其核心職責僅在於通過 Connect 方法建立一個邏輯連接,並返回一個 Stream 接口實例。這個 Stream接口則更爲基礎,借鑑了 golang.org/x/tools/internal/jsonrpc2_v2 的設計:

// A Stream is a bidirectional jsonrpc2 Stream.
type Stream interface {
 jsonrpc2.Reader
 jsonrpc2.Writer
 io.Closer
}

它組合了讀、寫和關閉能力。這種設計充滿了 “Go 味”:接口被設計得小巧而精煉,只暴露了最根本的抽象,完美體現了 Go “定義小接口,實現大價值” 的理念。

具體來看,Stream 接口因爲內嵌了 io.Closer,使其自然地遵循了標準庫的慣例,這使得它可以無縫集成到 Go 的資源管理模式中。更重要的是,Connect 方法的簽名嚴格遵循了 (ctx context.Context, ...params) (...results, error) 的形式。context.Context 作爲第一個參數,用於優雅地處理操作的超時和取消;而 error 作爲最後一個返回值,則用於明確、一致地傳遞錯誤信息。這些都是 Go I/O 和網絡編程中雷打不動的標準模式。這種底層接口的簡潔性不僅巧妙地隱藏了內部 JSON-RPC 實現的複雜細節(如 mcp/internal/jsonrpc2_v2 的使用),也爲用戶實現自定義的傳輸方式(如設計稿中提到的 InMemoryTransport 或 LoggingTransport)提供了極大的便利。

例如,NewCommandTransport 用於創建通過子進程 stdio 通信的客戶端傳輸:

// NewCommandTransport returns a [CommandTransport] that runs the given command
// and communicates with it over stdin/stdout.
func NewCommandTransport(cmd *exec.Cmd) *CommandTransport { /* ... */ }

得到的 CommandTransport 的Connect 方法會啓動命令並連接到其 stdin/stdout。這種清晰的職責劃分和對 Go 標準模式的遵循,使得整個傳輸層易於理解和擴展。

客戶端與服務端 API (Clients & Servers)

在客戶端和服務端核心對象的 API 設計上,Go 團隊同樣融入了對 Go 併發模型的深刻理解。設計稿清晰地區分了 Client/Server實例與ClientSession/ServerSession的概念,這在client.goserver.go中得到了體現。一個ClientServer實例可以處理多個併發的連接,即對應多個會話。這與我們熟悉的標準庫http.Client可以發起多個 HTTP 請求,而http.Server 可以同時爲多個客戶端提供服務的模式如出一轍。

// In client.go
type Client struct {
// ...
 mu       sync.Mutex
 sessions []*ClientSession
// ...
}
func NewClient(name, version string, opts *ClientOptions) *Client { /* ... */ }
func (c *Client) Connect(ctx context.Context, t Transport) (*ClientSession, error) { /* ... */ }

// In server.go
type Server struct {
// ...
 mu       sync.Mutex
 sessions []*ServerSession
// ...
}
func NewServer(name, version string, opts *ServerOptions) *Server { /* ... */ }
func (s *Server) Connect(ctx context.Context, t Transport) (*ServerSession, error) { /* ... */ }

這種 N:1(多個會話對應一個 Client/Server 實例)的設計,天然地利用並體現了 Go 語言強大的併發處理能力,通過 sync.Mutex 保護共享狀態。考慮到 Client 和 Server 本身都是有狀態的(例如,Client 可以動態添加或移除其追蹤的根資源,Server 則可以動態添加或移除其提供的工具),當這些核心實例的狀態發生變化時,設計確保了所有與其連接的對等方(即各個會話)都會收到相應的通知,從而維持了狀態的一致性。

在配置方式上,Go 團隊爲 Client 和 Server 的創建選擇了使用獨立的 ClientOptions 和 ServerOptions 結構體,如:

// In client.go
type ClientOptions struct {
 CreateMessageHandler func(context.Context, *ClientSession, *CreateMessageParams) (*CreateMessageResult, error)
 ToolListChangedHandler func(context.Context, *ClientSession, *ToolListChangedParams)
 // ... other handlers
}

// In server.go
type ServerOptions struct {
 Instructions string
 InitializedHandler func(context.Context, *ServerSession, *InitializedParams)
 // ... other handlers and fields like PageSize, LoggerName, LogInterval
}

而不是像社區中某些庫(包括設計稿中對比的 mcp-go)那樣採用可變參數選項 (variadic options) 的模式。他們認爲,對於配置項較多或邏輯較複雜的情況,顯式的結構體選項在可讀性上更勝一籌,也使得包的公開文檔更容易組織和理解。這是一個在 API 的簡潔性(可變參數有時更短)與明確性和長期可維護性之間做出的典型且值得借鑑的權衡。

Protocol Types 與 JSON Schema

MCP 協議的消息體是基於 JSON Schema 定義的。Go SDK 需要將這些 schema 映射爲 Go 的結構體。設計稿中提到協議類型是從 MCP 規範的 JSON schema 生成的,並且在 mcp 包內,除非 API 用戶需要,否則這些類型是未導出的。

以 content.go 中的 Content 類型爲例:

// Content is the wire format for content.
// It represents the protocol types TextContent, ImageContent, AudioContent
// and EmbeddedResource.
type Content struct {
 Type        string            `json:"type"`
 Text        string            `json:"text,omitempty"`
 MIMEType    string            `json:"mimeType,omitempty"`
 Data        []byte            `json:"data,omitempty"`
 Resource    *ResourceContents `json:"resource,omitempty"`
 Annotations *Annotations      `json:"annotations,omitempty"`
}

func (c *Content) UnmarshalJSON(data []byte) error {
// ... custom unmarshaling logic to validate Type field ...
}

func NewTextContent(text string) *Content {
return &Content{Type: "text", Text: text}
}
// ... other constructors like NewImageContent, NewAudioContent ...

這裏有幾個值得注意的 “Go 味” 設計:

此外,jsonschema/ 子包提供了完整的 JSON Schema 實現,包括從 Go 類型推斷 Schema (infer.go) 和校驗 (validate.go)。jsonschema/generate.go (在構建時忽略) 則展示瞭如何從遠程的 MCP JSON Schema URL 生成 protocol.go 中的 Go 類型定義,這體現了代碼生成的工程實踐。

RPC 方法簽名

對於 MCP 規範中定義的具體 RPC 方法,Go 團隊在 SDK 中的簽名設計上,將一致性和對向後兼容的執着追求體現得淋漓盡致。所有這些方法都嚴格遵循 func (s *SessionType) MethodName(ctx context.Context, params *XXXParams) (*XXXResult, error) 的模式。例如,在 client.go 中:

// ListPrompts lists prompts that are currently available on the server.
func (c *ClientSession) ListPrompts(ctx context.Context, params *ListPromptsParams) (*ListPromptsResult, error) {
 return standardCall[ListPromptsResult](ctx, c.conn, methodListPrompts, params)
}

這裏,context.Context 作爲第一個參數,error 作爲最後一個返回值,而參數 (*ListPromptsParams) 和結果 (*ListPromptsResult) 均使用指針類型——這些都是 Go API 設計的 “黃金法則”,確保了接口風格的統一和與 Go 生態的無縫對接。

唯一的例外是 ClientSession.CallTool 方法:

// CallTool calls the tool with the given name and arguments.
// Pass a [CallToolOptions] to provide additional request fields.
func (c *ClientSession) CallTool(ctx context.Context, name string, args map[string]any, opts *CallToolOptions) (*CallToolResult, error) { /* ... */ }

爲了提升用戶直接調用工具時的便捷性,它接受工具的名稱字符串和 map[string]any{} 類型的具體參數,以及一個可選的 *CallToolOptions,而不是要求用戶預先封裝一個 CallToolParams 結構體。這是一種在嚴格遵循模式與提升特定場景易用性之間做出的實用性調整。

設計稿中一個特別值得稱道的細節,是對向後兼容性的深思熟慮。團隊明確指出:“我們認爲,任何需要調用者傳遞新參數的規範更改都是不向後兼容的。因此,對於當前非必需的任何 XXXParams 參數,始終可以傳遞 nil。” 這意味着,即使未來 MCP 規範爲某個方法增加了新的可選參數(這些參數會被加入到對應的 XXXParams 結構體中),現有的、傳遞 nil 作爲參數的調用代碼也無需修改,依然能夠正常工作。這種對 API 演進的未雨綢繆,充分體現了 Go 團隊對兼容性承諾的高度重視和豐富經驗。至於爲何不直接暴露完整的 JSON-RPC 請求對象,團隊的考量是儘可能隱藏與業務邏輯無關的底層協議細節(如請求 ID),方法名由 Go 方法本身即可隱含,無需在參數中冗餘體現,保持了 API 的純粹性。

錯誤處理 (Errors) 與取消 (Cancellation)

在錯誤處理和操作取消這兩個關鍵機制上,SDK 的設計力求透明化,並與 Go 語言的核心理念保持高度一致。除了工具處理程序自身的業務邏輯錯誤外,所有協議級別的錯誤都會被透明地處理爲標準的 Go error類型。例如,服務器端特性處理程序中發生的錯誤,會作爲錯誤從 ClientSession 的相應調用中傳播出來,反之亦然,使得錯誤處理路徑清晰統一。

爲了幫助上層代碼更精確地理解錯誤的具體性質,設計稿提到協議層面的錯誤會包裝一個 JSONRPCError 類型(其定義在 protocol.go 中自動生成),該類型能夠暴露底層的 JSON-RPC 錯誤碼,便於進行鍼對性的處理。

// (Generated in protocol.go, but conceptually similar to design doc)
type JSONRPCError struct {
 Code    int64           `json:"code"`
 Message string          `json:"message"`
 Data    json.RawMessage `json:"data,omitempty"`
}

而對於操作的取消,則完全依賴並無縫集成了 Go 標準的 context.Context 機制。在 transport.go 的 call 函數中,可以看到這樣的邏輯:

// ... (inside call function)
 case ctx.Err() != nil:
  // Notify the peer of cancellation.
  err := conn.Notify(xcontext.Detach(ctx), "notifications/cancelled", &CancelledParams{
   Reason:    ctx.Err().Error(),
   RequestID: call.ID().Raw(),
  })
  return errors.Join(ctx.Err(), err)
// ...

當客戶端代碼取消一個傳遞給 SDK 方法的 context 時,SDK 會負責向服務器發送一個 "notifications/cancelled" 通知,同時客戶端的該方法調用會立即返回 ctx.Err()。相應地,服務器端在處理該請求時,其持有的 context 會被取消,從而可以進行適當的清理或中止操作。這種設計讓熟悉 Go 併發編程的開發者在處理取消邏輯時倍感親切和自然,無需學習新的機制。

可擴展性:中間件模式的青睞

爲了滿足用戶對 SDK 功能進行定製和擴展的需求,同時保持核心 API 的簡潔性,Go 團隊在可擴展性機制的設計上也體現了其偏好。在服務端(server.go)和客戶端(client.go),都提供了 AddMiddleware方法:

// In shared.go (conceptual definition)
type MethodHandler[S ClientSession | ServerSession] func(
 ctx context.Context, _ *S, method string, params any) (result any, err error)

type Middleware[S ClientSession | ServerSession] func(MethodHandler[S]) MethodHandler[S]

// In server.go
func (s *Server) AddMiddleware(middleware ...Middleware[ServerSession]) { /* ... */ }
// In client.go
func (c *Client) AddMiddleware(middleware ...Middleware[ClientSession]) { /* ... */ }

這些方法允許用戶註冊一個或多個遵循特定簽名的 Middleware 函數。這些函數本質上構成了 MCP 協議級別的中間件 (middleware) 鏈,它們會在服務器 / 客戶端收到請求、請求被解析之後,但在進入正常的業務處理邏輯之前依次執行(從右到左應用,即第一個中間件最先執行)。mcp_test.go 中的 traceCalls 就是一個很好的示例,它展示瞭如何用中間件來記錄請求和響應。

這種設計與 Go Web 開發(如 net/http 的 HandlerFunc 鏈)以及許多其他 Go 生態庫中廣泛採用的中間件模式一脈相承。它提供了一種強大且靈活的方式來注入橫切關注點,如日誌記錄、認證、請求修改等。相比之下,社區的 mcp-go 實現(如設計稿中提到的)定義了多達 24 個具體的 Server Hooks,每個 Hook 對應一個特定的事件點。Go 團隊的選擇顯然更傾向於通過一種更爲通用和模式化的方式來滿足擴展需求,從而避免了在覈心 Server/Session 類型上暴露過多的、細粒度的鉤子方法,保持了其接口的最小化和正交性。而對於像 HTTP 級別的身份驗證這類與 MCP 協議本身不直接相關的橫切關注點,設計稿則推薦使用標準的 HTTP 中間件模式來處理,進一步體現了關注點分離和利用現有生態成熟方案的設計思想。

通過對這些設計細節的 “庖丁解牛”,我們不難發現,Go 團隊在打造這個 MCP SDK 的過程中,無時無刻不在思考如何將 Go 語言的設計哲學、慣用模式以及對工程實踐的深刻理解融入其中,力求在滿足協議規範的完整性的同時,爲 Go 開發者提供一個簡潔、健壯、易用且面向未來的編程接口。

API 設計的 “Go 境界”:我們能學到什麼?

Go 團隊對 MCP SDK 的設計過程,如同一面鏡子,映照出 API 設計的諸多考量和 Go 語言的獨特氣質。從中,我們可以提煉出一些寶貴的啓示:

  1. “Go 味” 始於目標: 完整性、符合慣例、健壯性、面向未來、可擴展與最小化——這些目標共同構成了設計優秀 Go API 的基石。

  2. 標準庫是最好的老師: 學習並模仿 net/http, io, context 等核心庫的設計模式和 API 風格,是通往 “Idiomatic Go” 的捷徑。

  3. 接口的力量: 用小而美的接口來抽象行爲、解耦組件,是 Go 設計哲學的精髓。

  4. context 與 error 的 “一等公民” 地位: 在任何涉及 I/O、併發或可能失敗的操作中,將它們融入 API 設計是標準做法。

  5. 向後兼容性是生命線: API 一旦發佈,就需要慎重對待變更。在設計之初就考慮未來的演進,預留擴展點,比事後打補丁要優雅得多。

  6. 權衡的藝術: API 設計充滿了權衡——簡潔性與表達力、靈活性與易用性、當前需求與未來可能…… 沒有絕對的 “正確”,只有在特定上下文下的 “更優”。Go 團隊在包佈局、配置方式等方面的選擇,都體現了這種權衡。

小結

API 設計沒有銀彈,更像是一門手藝,需要在不斷的實踐、反思和學習中精進。Go 團隊爲 MCP SDK 所做的這些思考和設計決策,爲我們提供了一個寶貴的學習範例,展示瞭如何在 Go 的世界裏,打造出既滿足複雜需求,又不失簡潔與優雅的 API。

這種對 “Go 境界” 的追求——即代碼不僅能工作,而且寫得像 Go、用得像 Go,感覺像 Go——正是 Go 語言強大生命力和獨特魅力的源泉。

希望這篇文章能爲你未來的 API 設計帶來一些啓發。也歡迎你在評論區分享你對 API 設計的理解,或者你認爲一個 “好的 Go API” 應該具備哪些特質。

參考資料地址:https://github.com/orgs/modelcontextprotocol/discussions/364

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/mrphW4tymbv1cVbFjpnl2w