Skip to content

[go-fan] Go Module Review: modelcontextprotocol/go-sdk #3313

@github-actions

Description

@github-actions

🐹 Go Fan Report: modelcontextprotocol/go-sdk

Module Overview

github.com/modelcontextprotocol/go-sdk (v1.4.1) is the official Go implementation of the Model Context Protocol. It provides both server-side and client-side implementations including typed API structs, multiple transport types (stdio, StreamableHTTP, SSE, in-memory), session lifecycle management, and built-in slog integration.

This is the most central dependency in gh-aw-mcpg — it is the protocol layer that the entire gateway is built upon.

Current Usage in gh-aw

  • Files: 26 files across internal/mcp/, internal/server/, internal/testutil/mcptest/, and integration tests
  • Import Count: 26 imports (sdk "github.com/modelcontextprotocol/go-sdk/mcp")
  • Key APIs Used:
    • sdk.NewServer / sdk.NewClient — gateway server + backend clients
    • sdk.CommandTransport — stdio process transport for backend MCP servers
    • sdk.StreamableClientTransport / sdk.SSEClientTransport — HTTP client transports
    • sdk.NewStreamableHTTPHandler — agent-facing HTTP handler
    • sdk.ClientSession.ListTools / CallTool / ListResources / etc. — backend protocol calls
    • sdk.NewInMemoryTransports — in-process transport for tests
    • sdk.ClientOptions.KeepAlive — keepalive pings for HTTP backends

Architecture: Dual Role

The gateway plays two roles with the SDK simultaneously:

  1. MCP Server (facing LLM agents): sdk.NewServer + sdk.NewStreamableHTTPHandler expose tools via StreamableHTTP
  2. MCP Client (facing backend servers): sdk.NewClient + transports connect to Docker-containerized MCP backends

Transport Fallback Chain

For HTTP backends, the gateway tries transports in priority order:

StreamableClientTransport (2025-03-26 spec) → 5s probe timeout
  ↓ fail
SSEClientTransport (2024-11-05 spec, deprecated) → log deprecation warning
  ↓ fail  
Plain JSON-RPC over HTTP POST → custom implementation

Research Findings

Notable Design Patterns

Schema Bypass via Internal API: registerToolWithoutValidation calls server.AddTool() (instance method) rather than sdk.AddTool() (package function) to bypass JSON Schema validation. This allows backend tool schemas in JSON Schema draft-07 format to be registered without validation errors. The code includes this comment:

"This distinction relies on internal SDK behaviour and must be re-verified on every SDK upgrade."

Generic Pagination Helper: paginateAll[T] collects all pages from ListTools, ListResources, ListPrompts with a 100-page guard against runaway backends. Well-designed use of Go generics.

Custom 3-arg Handler Wrapper: Tool handlers use (ctx, req, state) → (result, data, error) instead of the SDK's (ctx, req) → (result, error), with an adapter layer. The extra state and data parameters support DIFC middleware and write-sink logging.

Session Caching in Routed Mode: filteredServerCache caches *sdk.Server instances per (backendID, sessionID) pair with TTL-based lazy eviction, preventing session fragmentation.

Protocol Version Status

The gateway implements MCPProtocolVersion = "2025-11-25" for initialization requests and uses the 2025-03-26 streamable HTTP transport as primary. SSE transport (2024-11-05) is detected and logged as deprecated.

Improvement Opportunities

🏃 Quick Wins

  1. Schema bypass test: registerToolWithoutValidation relies on internal SDK behavior per the comment. Add a simple unit test (or assertion at startup) that detects if the SDK version has changed this behavior — e.g., register a tool with an intentionally invalid schema and verify it succeeds via server.AddTool but would fail via sdk.AddTool. This creates a canary for SDK upgrades.

  2. Hardcoded response ID: marshalToResponse uses ID: 1 as a placeholder for all SDK-to-internal-Response conversions. Add a comment explaining that this is safe because the SDK's session.XXX() calls are already fully resolved at this point — the ID is never used for matching. This prevents future maintainers from flagging it as a correlation bug.

  3. Transport probe timeout: The 5-second timeout for transport detection (trySDKTransport) is hardcoded. Consider adding transport_probe_timeout to ServerConfig for backends that need more connection time.

✨ Feature Opportunities

  1. Surface InitializeResult capabilities: session.InitializeResult() is already used in validator.go to get serverInfo. Consider emitting backend capabilities (server name, version, supported features) to the operational log during startup — useful for debugging compatibility issues between the gateway and specific backends.

  2. Expand InMemoryTransports test coverage: The mcptest package uses sdk.NewInMemoryTransports for efficient in-process testing. Integration tests in test/integration/ that currently require the compiled binary could be restructured to use in-memory transports for a subset of scenarios, improving test speed and reliability.

  3. AddResource/AddPrompt gateway exposure: The gateway proxies resources and prompts from backends via callSDKMethod dispatch, but these are not surfaced through the gateway's own sdk.Server. If agent-facing resource/prompt access is needed, the SDK's AddResource/AddPrompt patterns (already demonstrated in mcptest/server.go) could be applied to UnifiedServer.

📐 Best Practice Alignment

  1. Watch for SkipValidation SDK option: The schema validation bypass via server.AddTool() vs sdk.AddTool() is a known fragility. Monitor future SDK releases for an explicit SkipValidation or AllowAnyInputSchema option that would replace the internal dependency. File this in the SDK upgrade checklist.

  2. SSE transport removal planning: The gateway correctly warns when SSE transport is detected. Consider adding a configuration option to disable SSE fallback entirely, which would force backends to upgrade and reduce code surface.

  3. Local CallToolParams type: mcp/types.go defines a local CallToolParams with Arguments map[string]interface{}. The marshal/unmarshal roundtrip in callParamMethod bridges this to the SDK's typed params. A comment in types.go explaining why a local type is maintained would help future maintainers understand the design intent.

🔧 General Improvements

  1. callSDKMethod method registry: The switch on method strings in callSDKMethod is a manual routing table. Adding new MCP methods (e.g., sampling/createMessage if the SDK adds support) requires updating this switch. Consider a map[string]func dispatch table or at minimum an "unsupported method" log/metric for discoverability.

  2. Connection error context: The LogConnectionError function in connection.go provides rich diagnostic context for failed backend connections. Consider applying the same pattern to session reconnect failures in reconnectSDKTransport and reconnectPlainJSON.

Recommendations (Prioritized)

Priority Item Effort Impact
🔴 High Schema bypass canary test (SDK upgrade safety) Low High
🟡 Medium Surface InitializeResult in startup logs Low Medium
🟡 Medium Document placeholder response ID Low Low
🟢 Low Transport probe timeout configurability Medium Low
🟢 Low SSE fallback disable option Medium Low
🟢 Low Expand InMemoryTransports test coverage High Medium

Next Steps

  • Add schema bypass canary test to internal/server/tool_registry_test.go
  • Log InitializeResult server info during backend registration in registerToolsFromBackend
  • Add comment to marshalToResponse explaining ID: 1 placeholder safety
  • Add SDK upgrade checklist item to track SkipValidation option availability

Generated by Go Fan 🐹
Module summary saved to: docs/go-sdk-review.md
Workflow run: §24070033091

Note

🔒 Integrity filter blocked 18 items

The following items were blocked because they don't meet the GitHub integrity level.

To allow these resources, lower min-integrity in your GitHub frontmatter:

tools:
  github:
    min-integrity: approved  # merged | approved | unapproved | none

Generated by Go Fan · ● 3.8M ·

  • expires on Apr 14, 2026, 7:47 AM UTC

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions