Skip to content

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

@github-actions

Description

@github-actions

🐹 Go Fan Report: modelcontextprotocol/go-sdk

Module Overview

The official Go SDK for the Model Context Protocol (github.com/modelcontextprotocol/go-sdk v1.4.0). This is the most critical direct dependency in the project β€” it provides the protocol backbone for the entire gateway: typed MCP server/client implementations, all transport types (stdio, streamable HTTP, SSE, in-memory), and session management.

Current Usage in gh-aw

The SDK is imported in 22 files across server, client, middleware, and test utilities, making it the most pervasive dependency in the codebase.

  • Files: 22 files
  • Import alias: sdk "github.com/modelcontextprotocol/go-sdk/mcp"
  • Key APIs Used:
    • Server side: sdk.NewServer, server.AddTool, sdk.NewStreamableHTTPHandler, sdk.StreamableHTTPOptions
    • Client side: sdk.NewClient, client.Connect, session.ListTools/CallTool/ListResources/ReadResource/ListPrompts/GetPrompt
    • Transports: sdk.CommandTransport (stdio), sdk.StreamableClientTransport, sdk.SSEClientTransport, sdk.NewInMemoryTransports (tests)
    • Types: sdk.Tool, sdk.Resource, sdk.CallToolResult, sdk.TextContent, sdk.Content, sdk.Implementation

Research Findings

Architecture

The gateway acts as a proxy: it uses the SDK as a client to connect to backend MCP servers (via CommandTransport for Docker stdio containers, or HTTP transports), and as a server to expose aggregated tools to agents. The transport fallback chain for HTTP backends is: Streamable HTTP (2025-03-26 spec) β†’ SSE (deprecated) β†’ plain JSON-RPC.

Notable Design Decisions

  • server.AddTool over sdk.AddTool: The method form is used intentionally to bypass SDK schema validation, enabling support for backend tools that use JSON Schema draft-07 (incompatible with the SDK's schema validator). Well-documented in comments.
  • slog integration: SDK logging is correctly wired through the project's logger.NewSlogLoggerWithHandler adapter in both ServerOptions.Logger and ClientOptions.Logger.
  • Protocol version: Uses 2025-11-25 (latest draft) in initialize handshakes.
  • Session timeout: 30 minutes for StreamableHTTPOptions.SessionTimeout β€” reasonable for preventing resource leaks.

Recent Updates (v1.4.0)

Protocol version 2025-11-25, full streamable HTTP support, in-memory transport pairs for testing, session ID tracking via session.ID().


Improvement Opportunities

πŸƒ Quick Wins

1. Missing pagination in tool/resource/prompt discovery

All three list operations use empty params with no cursor:

// internal/mcp/connection.go
result, err := c.session.ListTools(c.ctx, &sdk.ListToolsParams{})
result, err := c.session.ListResources(c.ctx, &sdk.ListResourcesParams{})
result, err := c.session.ListPrompts(c.ctx, &sdk.ListPromptsParams{})

The MCP spec supports cursor-based pagination. Backends with more tools than a single page will have tools silently dropped at gateway startup. A pagination loop (checking result.NextCursor until empty) should be added for robustness:

var allTools []*sdk.Tool
var cursor string
for {
    params := &sdk.ListToolsParams{}
    if cursor != "" {
        params.Cursor = cursor
    }
    result, err := c.session.ListTools(c.ctx, params)
    if err != nil { return nil, err }
    allTools = append(allTools, result.Tools...)
    if result.NextCursor == "" { break }
    cursor = result.NextCursor
}

2. ValidatorClient missing logger in test options

internal/testutil/mcptest/validator.go:22 creates &sdk.ClientOptions{} with no logger. Test failures become harder to diagnose. A test-appropriate logger should be passed:

}, &sdk.ClientOptions{
    Logger: logger.NewSlogLoggerWithHandler(log), // add debug visibility
})

✨ Feature Opportunities

3. MCP Notification forwarding (notifications/tools/list_changed)

The gateway currently only proxies tool calls β€” it does not forward MCP notifications from backends to clients. When a backend reloads its tool list at runtime, connected clients see a stale tool list until they reconnect. The SDK's client session exposes SetNotificationHandler; wiring this up would enable the gateway to:

  1. Receive notifications/tools/list_changed from a backend
  2. Re-fetch that backend's tools
  3. Push notifications/tools/list_changed to all connected gateway clients

This would give clients live tool list updates without reconnection.

4. Connection health / keepalive probing

The gateway has no heartbeat mechanism for backend connections. A stale connection (e.g., Docker container restarted) is only detected when a tool call fails. Implementing a lightweight periodic ping (if the SDK exposes a ping/pong mechanism) or checking session liveness would allow proactive reconnection.


πŸ“ Best Practice Alignment

5. Simplify custom 3-parameter handler type

The gateway uses a custom 3-parameter handler signature throughout:

func(context.Context, *sdk.CallToolRequest, interface{}) (*sdk.CallToolResult, interface{}, error)

The third parameter (interface{}) is always nil when called, and the second return value (interface{}) is used only for middleware data extraction (the actual payload). The SDK-native signature is:

func(context.Context, *sdk.CallToolRequest) (*sdk.CallToolResult, error)

Consider whether the middleware pattern could be redesigned to use context values instead of a custom handler type, which would eliminate the wrapper boilerplate and align with the SDK's API contract.


πŸ”§ General Improvements

6. filteredServerCache memory leak in routed mode

internal/server/routed.go's filteredServerCache stores one *sdk.Server per (backendID, sessionID) pair and never evicts entries:

type filteredServerCache struct {
    servers map[string]*sdk.Server
    mu      sync.RWMutex
}

In deployments where the Authorization header varies per request (e.g., user tokens), this grows unboundedly. Each sdk.Server holds registered tools with closures β€” a non-trivial allocation. A max-size LRU eviction or TTL-based cleanup should be added.


Recommendations (Priority Order)

  1. [High] Implement pagination for ListTools, ListResources, ListPrompts β€” silent data loss risk
  2. [Medium] Fix filteredServerCache eviction β€” memory leak in multi-user deployments
  3. [Medium] Add notification forwarding for tools/list_changed β€” improves live refresh UX
  4. [Low] Add logger to ValidatorClient β€” better test debuggability
  5. [Low] Explore simplifying custom handler type β€” reduces SDK abstraction overhead

Next Steps

  • Add pagination loop in connection.go listTools, listResources, listPrompts
  • Audit filteredServerCache for eviction strategy
  • Investigate session.SetNotificationHandler availability in v1.4.0 for notification forwarding

Generated by Go Fan β€” Β§23233789121
Module summary saved to: session-state/files/go-sdk-module-review.md
(Note: specs/mods/ directory write was blocked by sandbox; summary stored in session files)

Generated by Go Fan Β· β—·

  • expires on Mar 25, 2026, 7:39 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