Skip to content

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

@github-actions

Description

@github-actions

🐹 Go Fan Report: modelcontextprotocol/go-sdk

TL;DR: The MCP go-sdk is deeply and thoughtfully integrated across 27 files. The project is running v1.4.1 while v1.5.0 is available. Two structural concerns merit attention: a fragile reliance on SDK internals for schema-validation bypass, and string-matching for session-expiry detection.


Module Overview

github.com/modelcontextprotocol/go-sdk is the official Go SDK for the Model Context Protocol (MCP). It provides client and server primitives for JSON-RPC 2.0 communication over three transports: stdio (via CommandTransport), StreamableHTTP (2025-03-26 spec), and SSE (2024-11-05 spec, deprecated). It is the foundational dependency — everything this gateway does flows through it.

  • Version in use: v1.4.1
  • Latest available: v1.5.0
  • Import alias: sdk "github.com/modelcontextprotocol/go-sdk/mcp"

Current Usage in gh-aw

The SDK is imported in 27 files across production and test code.

Area Files Purpose
internal/mcp/ connection.go, http_transport.go, tool_result.go Backend client sessions, transport negotiation, content conversion
internal/server/ unified.go, routed.go, transport.go, tool_registry.go Gateway server construction, tool registration, HTTP handlers
internal/testutil/mcptest/ server.go, driver.go, validator.go In-memory test infrastructure
test/integration/ Various End-to-end binary tests

Key APIs Used

API Usage Count Role
sdk.CallToolResult / sdk.CallToolRequest 30+ Tool call I/O types — ubiquitous
sdk.TextContent / sdk.Content 14 Content type construction
sdk.Server / sdk.NewServer 11 Gateway server instances
sdk.Transport 8 Abstraction over stdio/HTTP/in-memory
sdk.NewStreamableHTTPHandler 2 Mounts /mcp and /mcp/{id} endpoints
sdk.StreamableClientTransport 2 HTTP backend (2025 spec)
sdk.SSEClientTransport 2 HTTP backend fallback (deprecated)
sdk.NewInMemoryTransports 1 Unit test transport pair
sdk.ToolAnnotations 2 Passed through from backends

Research Findings

Architecture: Three-Argument Handler Pattern

The project introduces a deliberate extension of the SDK's two-argument handler contract:

// SDK's native form:
func(ctx context.Context, req *sdk.CallToolRequest) (*sdk.CallToolResult, error)

// gh-aw's extended form:
func(ctx context.Context, req *sdk.CallToolRequest, state interface{}) (*sdk.CallToolResult, interface{}, error)

The extra state and second return value thread data through the jq middleware pipeline and DIFC write-sink logger. This is well-documented in registerToolWithoutValidation. It's non-idiomatic but clearly justified.

Transport Fallback Chain

NewHTTPConnection implements a robust three-tier fallback: StreamableHTTP → SSE → plain JSON-RPC. Each tier uses dedicated reconnection logic. This is excellent resilience engineering.

Pagination Safety

A custom generic paginateAll[T] wraps SDK list methods with a 100-page hard cap to guard against runaway backends. This is a practical defensive pattern.


Improvement Opportunities

🏃 Quick Wins

  1. Upgrade to v1.5.0 (go.mod bump from v1.4.1 → v1.5.0)

    • Candidate improvements: updated protocol support, new APIs, potential bug fixes
    • Risk: the registerToolWithoutValidation approach (see below) must be re-verified post-upgrade
    • Action: review the SDK's CHANGELOG / release notes for breaking changes before upgrading
  2. Tighten the registerToolWithoutValidation comment

    • Current comment says: "The Server.AddTool method (used here) skips JSON Schema validation whereas the sdk.AddTool function validates the schema. This distinction relies on internal SDK behaviour and must be re-verified on every SDK upgrade."
    • Improvement: add a reference test or assertion that confirms the bypass still works after upgrade, rather than relying on a code comment reminder alone

✨ Feature Opportunities

  1. Check v1.5.0 for a public schema-bypass API

    registerToolWithoutValidation (internal/server/tool_registry.go:51) relies on a behavioral difference between calling server.AddTool (the method on *sdk.Server) vs sdk.AddTool (the package-level function). The method bypasses JSON Schema validation; the function does not. This is an undocumented SDK internal behavior. If v1.5.0 adds an explicit SkipValidation option to server.AddTool, this fragility can be eliminated.

  2. Check v1.5.0 for typed session-expiry errors

    isSessionNotFoundError (internal/mcp/http_transport.go:72) detects session expiry by string-matching "session not found" against the error message. If the SDK added a typed sentinel error (e.g., sdk.ErrSessionNotFound) in v1.5.0, the reconnect logic can be made robust against error message changes:

    // Current (fragile):
    func isSessionNotFoundError(err error) bool {
        return strings.Contains(strings.ToLower(err.Error()), "session not found")
    }
    
    // Preferred (if SDK provides it):
    if errors.Is(err, sdk.ErrSessionNotFound) { ... }
  3. Check v1.5.0 for built-in pagination helpers

    The custom paginateAll[T] generic function (internal/mcp/connection.go:555) is clean and correct, but if the SDK now provides built-in auto-pagination for ListTools, ListResources, and ListPrompts, the custom implementation could be removed, reducing maintenance surface.


📐 Best Practice Alignment

  1. MCPProtocolVersion in the plain-JSON fallback

    MCPProtocolVersion = "2025-11-25" (internal/mcp/http_transport.go:37) is used in the manual plain-JSON-RPC initialize request. This value is hardcoded and separate from the SDK's own protocol negotiation (which handles its version automatically). Verify that this matches the SDK's negotiated version so backends receive a consistent protocol version regardless of transport tier.

  2. StreamableHTTPOptions.Stateless is correctly set to false for both routed and unified handlers. Confirm the default has not changed to true in v1.5.0, which would break session continuity silently.


🔧 General Improvements

  1. SSE deprecation warning placement

    When an SSE connection succeeds, a multi-line ⚠️ WARNING is written via log.Printf (standard library logger) rather than logger.LogWarn (the project's structured logger). This means the SSE deprecation warning bypasses the structured logging pipeline and won't appear in mcp-gateway.log. Consider routing it through logger.LogWarn for consistent log capture.

  2. ClientSession reuse on tool registry refresh

    During parallel tool registration, each registerToolsFromBackend call creates a fresh SDK session via launcher.GetOrLaunch. If a backend refreshes its tool list and the cursor from a previous paginated response is stale, paginateAll may emit a spurious error. The current code fetches all pages in a single call, so this is low risk — but worth a note in the code.


Recommendations (Prioritized)

Priority Item Effort
🔴 High Upgrade to v1.5.0 and verify registerToolWithoutValidation still bypasses schema validation Medium
🟠 Medium Check v1.5.0 for typed ErrSessionNotFound → replace string matching in isSessionNotFoundError Small
🟠 Medium Route SSE deprecation log.Printf warnings through logger.LogWarn for structured logging Small
🟡 Low Check v1.5.0 for public SkipValidation API → remove fragile method-vs-function distinction Medium
🟡 Low Check v1.5.0 for built-in pagination → potentially remove custom paginateAll Medium

Next Steps

  • go get github.com/modelcontextprotocol/go-sdk@v1.5.0 — review diff, run make agent-finished
  • Post-upgrade: confirm registerToolWithoutValidation still bypasses JSON Schema validation (add a regression test)
  • File a follow-up on each "check v1.5.0" item once the upgrade is done

Generated by Go Fan 🐹 · Run §24178458284
Module summary saved to: specs/mods/go-sdk.md (note: directory needs creation)

Note

🔒 Integrity filter blocked 8 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 · ● 2.3M ·

  • expires on Apr 16, 2026, 8:09 AM UTC

Metadata

Metadata

Assignees

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