The "Go Realm" of API Design: Trade-offs and Considerations in the Go Team's MCP SDK Design Process

ImagePlease click the blue text above TonyBai to subscribe to the official account!

Image

Hello everyone, I'm Tony Bai.

As developers, we interact with APIs daily—calling them, designing them, and sometimes agonizing over poorly designed APIs. A well-designed API is like a skilled guide, clearly and efficiently leading us to the desired complex functionality; a bad API, however, can be like a maze full of traps, making our progress difficult.

So, in the world of Go, what does a "good" API look like? How should it embody Go's philosophy of simplicity, efficiency, and concurrency safety? How can it meet functional requirements while remaining developer-friendly and future-compatible?

Recently, the official Go team initiated a design discussion for a Go SDK for the Model Context Protocol (MCP) and publicly released its detailed design draft along with an initial prototype code implementation. This design document and code, in my opinion, is not just a plan for the Go language implementation of the MCP protocol, but a "public lesson" from the official Go team on API design thinking and practice. It vividly demonstrates the trade-offs involved in building a powerful and Idiomatic Go SDK, and how to integrate Go's design philosophy into every detail.

Today, let's delve into this design document and its prototype code to explore the "Go Realm" that the Go team pursues in API design.

The "Original Intention" of API Design: Goals Set by the Go Team for the MCP SDK

Before diving into the details, let's look at the core objectives (Requirements) the Go team set for this official MCP SDK. These goals are themselves important guidelines for designing any high-quality Go SDK:

1. Completeness (Complete): Must implement all features of the MCP specification and strictly adhere to its semantics. This is a basic requirement for an SDK as a protocol implementation.

2. Idiomatic Go: This is the core of the "Go Realm." The SDK should maximize the use of Go language features and standard library design patterns, and replicate common idioms already established in similar areas of the Go ecosystem (e.g., net/http, grpc-go).

3. Robustness (Robust): The SDK itself must be well-tested, stable, and reliable, and it must allow users to easily test applications built upon it.

4. Future-proof: The design must account for the future evolution of the MCP specification, aiming to avoid backward-incompatible breaking changes to the SDK API due to specification changes as much as possible.

5. Extensibility and Minimality (Extensible & Minimal): To best serve the preceding four goals, the SDK's core API should remain minimal and orthogonal. At the same time, it must allow users to extend it in a simple and clear manner (e.g., via interfaces, middleware, hooks) to meet specific needs.

These goals clearly outline the Go team's expectations for a "good" Go SDK: it must not only be functionally complete but also "write like Go, use like Go," and stand the test of time.

Anatomy: The "Go Flavor" and Trade-offs in MCP Go SDK Design

Having set clear API design goals, the Go team began to put these principles into practice, embarking on the design of the specific structure and interfaces for the MCP Go SDK. By carefully reading this design document and its prototype code, we can clearly discern the strong "Go flavor" in several key decisions and deeply appreciate the subtle trade-offs they made between functional completeness, language idioms, current usability, and future evolution.

Package Layout

In the overall structure of the SDK, the Go team made a notable choice regarding package layout, which directly reflects their deep understanding of Go ecosystem conventions and their prioritization of developer experience. Unlike MCP SDKs in other languages that might meticulously split client, server, and transport layer functionalities into separate packages, the Go team proposed centralizing the SDK's core user interface within a singlemcp package.

This approach maintains high consistency with the organizational methods of core packages in the Go standard library, such asnet/http, net/rpc, and the widely adoptedgoogle.golang.org/grpc in the community. For Go developers, this means a lower cognitive barrier—when they need to use MCP functionality, almost all core APIs can be found under the samemcp package, which greatly enhances API discoverability. Furthermore, a centralized package structure facilitates the generation of aggregated package documentation and provides a smoother code hint and navigation experience in IDEs.

A deeper consideration is the SDK's long-term stability and future adaptability. If functionalities are excessively split into multiple fine-grained packages, any minor adjustment to the MCP specification in the future could trigger chain reactions in package structure changes or complex cross-package dependency issues. A single core package design, however, can better absorb these changes, reducing impact on user code. Of course, auxiliary functionalities like JSON Schema, which are not directly related to MCP's core logic but might be needed by SDK users, are reasonably planned into separate sub-packages (e.g.,jsonschema/) to achieve separation of concerns. While this strategy might make some developers who pursue extreme "modularity" feel the core package is slightly "bloated," the Go team clearly prioritized user discoverability, documentation clarity, and long-term evolutionary stability in this decision.

JSON-RPC and Transport Layer Abstraction (Transports)

The core of the MCP protocol lies in exchanging messages between clients and servers via JSON-RPC, with various underlying transport methods such as stdio, streamable HTTP, SSE, etc. Designing a unified yet flexible abstraction layer for these diverse transport forms is a major challenge for SDK designers. Here, the Go team once again demonstrated their skillful application of interface design art.

Intransport.go, they defined a very low-levelTransport interface: // A Transport is used to create a bidirectional connection between MCP client // and server. type Transport interface { Connect(ctx context.Context) (Stream, error) }

Its core responsibility is solely to establish a logical connection via theConnect method and return aStream interface instance. ThisStream interface is even more fundamental, drawing inspiration fromgolang.org/x/tools/internal/jsonrpc2_v2's design:

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

It combines read, write, and close capabilities. This design is full of "Go flavor": interfaces are designed to be small and refined, exposing only the most fundamental abstractions, perfectly embodying Go's philosophy of "defining small interfaces, achieving great value."

Specifically, because theStream interface embedsio.Closer, it naturally adheres to standard library conventions, allowing seamless integration into Go's resource management patterns. More importantly, the signature of theConnect method strictly follows the(ctx context.Context, ...params) (...results, error)form.context.Context as the first parameter is used for gracefully handling operation timeouts and cancellations;error as the last return value is used for clear and consistent error reporting. These are unshakeable standard patterns in Go I/O and network programming. The simplicity of this low-level interface not only cleverly hides the complex details of the internal JSON-RPC implementation (such as themcp/internal/jsonrpc2_v2 usage) but also provides great convenience for users to implement custom transport methods (such asInMemoryTransport orLoggingTransport mentioned in the design document).

For example,NewCommandTransport is used to create a client transport that communicates via subprocess stdio:

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

TheConnect method of the resultingCommandTransport will start the command and connect to its stdin/stdout. This clear division of responsibilities and adherence to Go's standard patterns makes the entire transport layer easy to understand and extend.

Client and Server API (Clients & Servers)

In the API design for core client and server objects, the Go team also incorporated a deep understanding of Go's concurrency model. The design document clearly distinguishes betweenClient/Server instances andClientSession/ServerSession concepts, which is reflected inclient.go andserver.go. AClient orServer instance can handle multiple concurrent connections, corresponding to multiple sessions. This is very similar to the familiar standard libraryhttp.Client that can initiate multiple HTTP requests, andhttp.Server that can simultaneously serve multiple clients.

// 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) { /* ... */ }

This N:1 design (multiple sessions corresponding to one Client/Server instance) naturally leverages and embodies Go's powerful concurrency handling capabilities, protecting shared state via sync.Mutex. Considering that both Client and Server are stateful (e.g., Client can dynamically add or remove root resources it tracks, and Server can dynamically add or remove tools it provides), when the state of these core instances changes, the design ensures that all connected peers (i.e., each session) receive corresponding notifications, thereby maintaining state consistency.

For configuration, the Go team chose to use separateClientOptions andServerOptions structs for creatingClient andServer, such as:

// 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 }

rather than adopting the variadic options pattern used by some community libraries (includingmcp-go mentioned in the design document). They believe that for cases with many or complex configuration items, explicit struct options offer better readability and make package documentation easier to organize and understand. This is a typical and noteworthy trade-off made between API conciseness (variadic options can sometimes be shorter) and clarity/long-term maintainability.

Protocol Types and JSON Schema

The message bodies of the MCP protocol are defined based on JSON Schema. The Go SDK needs to map these schemas to Go structs. The design document mentions that protocol types are generated from the MCP specification's JSON schema, and within the mcp package, these types are unexported unless required by the API user.

Taking theContent type inclient.go as an example:

// 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 ...

Here are a few notable "Go flavor" designs:

- Clear struct definitions: Directly map JSON structures, usingjsonstruct tags to control serialization behavior.

- Constructors: ProvideNewXXXContent helper functions to create specific types ofContent instances, ensuring theType field is correctly set, enhancing usability and safety.

- Custom JSON handling: TheContent type implements theUnmarshalJSON method for validating theType field during deserialization to ensure it is a legal type defined by the protocol. ForResourceContents, it even implementsMarshalJSON to handle the subtle difference between theBlob field beingnil and an empty slice (for compatibility with pre-Go 1.24omitzero behavior). This practice of intervening in the encoding/decoding process when necessary to ensure data correctness is a testament to Go's type system capabilities.

- Use ofjson.RawMessage: The design document mentions that for user-provided data, the SDK usesjson.RawMessage, which delegates the Marshal/Unmarshal responsibility to the client or server's business logic. This is a lazy parsing strategy that can improve performance and increase flexibility.

Additionally, thejsonschema/ sub-package provides a complete JSON Schema implementation, including inferring Schema from Go types (infer.go) and validation (validate.go).jsonschema/generate.go (ignored during build) demonstrates how to generate Go type definitions inprotocol.go from a remote MCP JSON Schema URL, reflecting the engineering practice of code generation.

RPC Method Signatures

For the specific RPC methods defined in the MCP specification, the Go team's SDK signature design exemplifies consistency and an unwavering pursuit of backward compatibility. All these methods strictly follow thefunc (s *SessionType) MethodName(ctx context.Context, params *XXXParams) (*XXXResult, error)pattern. For example, inclient.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) }

Here,context.Context as the first parameter anderror as the last return value, and both parameters (*ListPromptsParams) and results (*ListPromptsResult) use pointer types—these are the "golden rules" of Go API design, ensuring consistency in interface style and seamless integration with the Go ecosystem.

The only exception is theClientSession.CallTool method:

// 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) { /* ... */ }

To enhance user convenience when directly calling tools, it accepts the tool's name string and specific parameters of typemap[string]any{}, along with an optional*CallToolOptions, instead of requiring users to pre-encapsulate aCallToolParams struct. This is a practical adjustment made between strict adherence to patterns and improving usability in specific scenarios.

A particularly commendable detail in the design document is the thoughtful consideration of backward compatibility. The team explicitly states: "We believe that any specification change that requires callers to pass new parameters is not backward compatible. Therefore, for any XXXParams parameter that is not currently required,nil can always be passed." This means that even if the MCP specification adds new optional parameters to a method in the future (these parameters would be added to the correspondingXXXParams struct), existing call code passingnil as a parameter would not need modification and would still work correctly. This foresight in API evolution fully demonstrates the Go team's high regard for compatibility commitments and rich experience. As for why they don't directly expose the full JSON-RPC request object, the team's consideration is to hide underlying protocol details unrelated to business logic (such as request ID) as much as possible; the method name can be implicitly conveyed by the Go method itself, without redundant inclusion in parameters, maintaining the API's purity.

Error Handling and Cancellation

In these two critical mechanisms—error handling and operation cancellation—the SDK's design strives for transparency and maintains high consistency with Go's core philosophy. Apart from business logic errors within tool handlers themselves, all protocol-level errors are transparently handled as standard Goerror types. For example, errors occurring in server-side feature handlers will propagate as errors from the corresponding calls ofClientSession, and vice versa, making the error handling path clear and unified.

To help higher-level code more precisely understand the specific nature of errors, the design document mentions that protocol-level errors will wrap aJSONRPCError type (defined inprotocol.go, automatically generated), which can expose underlying JSON-RPC error codes for targeted handling.

// (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"` }

As for operation cancellation, it fully relies on and seamlessly integrates Go's standardcontext.Context mechanism. Intrnasport.go's call function, one can see logic like this:

// ... (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) // ...

When client code cancels acontext passed to an SDK method, the SDK is responsible for sending a "notifications/cancelled" notification to the server, and the client's method call will immediately returnctx.Err(). Correspondingly, when the server handles the request, its heldcontext will be canceled, allowing for appropriate cleanup or abortion of the operation. This design makes developers familiar with Go's concurrency programming feel at ease and natural when dealing with cancellation logic, without needing to learn new mechanisms.

Extensibility: Favoring the Middleware Pattern

To meet user demands for customizing and extending SDK functionality while maintaining API conciseness, the Go team also demonstrated its preference in the design of the extensibility mechanism. Both the server (server.go) and client (client.go) provide theAddMiddleware method:

// 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]) { /* ... */ }

These methods allow users to register one or moreMiddleware functions that conform to a specific signature. These functions essentially form an MCP protocol-level middleware chain, which executes sequentially (applied from right to left, meaning the first middleware executes first) after the server/client receives and parses a request but before entering the normal business logic.mcp_test.go'straceCalls is a good example, showing how middleware can be used to log requests and responses.

This design is consistent with the middleware pattern widely adopted in Go Web development (e.g.,net/http's HandlerFunc chain) and many other Go ecosystem libraries. It provides a powerful and flexible way to inject cross-cutting concerns, such as logging, authentication, and request modification. In contrast, the community'smcp-go implementation (as mentioned in the design document) defines as many as 24 specific Server Hooks, each corresponding to a particular event point. The Go team's choice clearly favors a more generic and pattern-based approach to meet extension needs, thereby avoiding exposing too many fine-grained hook methods on the core Server/Session types, maintaining the minimality and orthogonality of its interfaces. For cross-cutting concerns like HTTP-level authentication that are not directly related to the MCP protocol itself, the design document recommends using the standard HTTP middleware pattern, further reflecting the design philosophy of separation of concerns and leveraging existing mature ecosystem solutions.

Through this "anatomy" of these design details, it is not difficult to see that the Go team, in building this MCP SDK, constantly considered how to integrate Go language's design philosophy, idiomatic patterns, and deep understanding of engineering practices, striving to provide Go developers with a concise, robust, easy-to-use, and future-oriented programming interface while ensuring the completeness of the protocol specification.

The "Go Realm" of API Design: What Can We Learn?

The Go team's MCP SDK design process is like a mirror, reflecting various API design considerations and Go language's unique character. From it, we can distill some valuable insights:

1. "Go Flavor" starts with goals: Completeness, idiomatic compliance, robustness, future-proofing, extensibility, and minimality—these goals together form the foundation for designing excellent Go APIs.

2. The standard library is the best teacher: Learning from and imitating the design patterns and API styles of core libraries like net/http, io, and context is the shortcut to "Idiomatic Go."

3. The power of interfaces: Using small, elegant interfaces to abstract behaviors and decouple components is the essence of Go's design philosophy.

4. context and error as "first-class citizens": In any operation involving I/O, concurrency, or potential failure, integrating them into API design is standard practice.

5. Backward compatibility is a lifeline: Once an API is published, changes must be handled with caution. Considering future evolution and reserving extension points at the outset is far more elegant than patching it up later.

6. The art of trade-offs: API design is full of trade-offs—conciseness versus expressiveness, flexibility versus usability, current needs versus future possibilities... There is no absolute "right," only "better" in a specific context. The Go team's choices in package layout, configuration methods, etc., all reflect this trade-off.

Summary

API design has no silver bullet; it's more like a craft that requires continuous practice, reflection, and learning to refine. The Go team's considerations and design decisions for the MCP SDK provide us with a valuable learning example, demonstrating how to build an API in the Go world that not only meets complex requirements but also remains concise and elegant.

This pursuit of the "Go Realm"—that is, code that not only works but also writes like Go, uses like Go, and feels like Go—is precisely the source of Go language's powerful vitality and unique charm.

I hope this article provides some inspiration for your future API design. You are also welcome to share your understanding of API design in the comments section, or what qualities you believe a "good Go API" should possess.

Reference address: https://github.com/orgs/modelcontextprotocol/discussions/364

Cultivating Progress, Reaching New Heights: Unlocking the "Go Realm" of Go API Design

Are you still wanting more after today's Go API design case study? Do you want to systematically learn how to integrate Go's official design wisdom into every interface you build?

In my newly launched Go Language Advanced Course, I have a dedicated lecture titled "API Design: Building User-Loved, Robust, and Reliable Public Interfaces." It will delve deeply into the five core elements of Go API design and combine them with more practical case studies, helping you move from "knowing how to use Go" to "mastering Go."

Scan the QR code below to start your advanced journey now!

Image

Thank you for reading!

If this article has given you new insights into Go API design, please help like👍, watch👀, and share🚀 to let more friends learn and progress together!

Click the titles below to read more insights!

- Hands-on with GOEXPERIMENT=jsonv2: A First Look at Go's Next-Gen JSON Library

- New AI Favorite? Understanding Why MCP and A2A Prefer JSON-RPC 2.0

- Go encoding/json/v2 Proposal: A New Engine for JSON Processing

- I'll Tell You How to Structure a Standard Go Project

- A Comprehensive Guide to Go's Standard Library context Package

- Designing Responses for gRPC Services

- "Error as Value," Different Implementations: A Comparison of Go and Zig's Error Handling Philosophies

Main Tag:Go API Design

Sub Tags:Go ProgrammingSoftware EngineeringConcurrencySDK Development


Previous:Claude 4 Launched: Anthropic No Longer Teaches AI to Code, But Lets It Write Projects Independently

Next:Interpretation of Seed1.5-VL Technical Report

Share Short URL