Skip to content

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

@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 SDK for the Model Context Protocol. It provides the complete MCP server and client implementation β€” the core of how gh-aw-mcpg exposes MCP endpoints and connects to backend MCP servers. This is the most strategically important dependency in the project.

Current Usage in gh-aw

The SDK is used pervasively across the codebase, aliased everywhere as sdk "github.com/modelcontextprotocol/go-sdk/mcp".

  • Files: 26 files (12 production, 14 test)
  • Key APIs Used:
API Purpose
sdk.NewServer Creates MCP server for unified and routed endpoints
sdk.NewClient + client.Connect Connects to backend MCP servers
sdk.NewStreamableHTTPHandler HTTP transport handler for /mcp and /mcp/{server} routes
sdk.CommandTransport Stdio transport for containerized backends
sdk.StreamableClientTransport Streamable HTTP (2025-03-26 spec) client
sdk.SSEClientTransport SSE (2024-11-05 spec) client for legacy backends
sdk.Tool, sdk.CallToolRequest/Result Tool definition and invocation protocol
sdk.TextContent, sdk.ImageContent, sdk.AudioContent, sdk.EmbeddedResource MCP content types
sdk.ClientOptions.KeepAlive Periodic pings to keep HTTP backend sessions alive
sdk.StreamableHTTPOptions Session timeout and statefulness configuration

Research Findings

Architecture: How the SDK is layered

The project uses the SDK at two distinct levels:

  1. Inbound (gateway β†’ client): sdk.NewServer + sdk.NewStreamableHTTPHandler creates the MCP endpoint that agents connect to. Session callbacks in the handler authenticate incoming requests and route them to the right backend.

  2. Outbound (gateway β†’ backend): sdk.NewClient + transport connectors connect the gateway to backend MCP servers. The project implements transport auto-negotiation (Streamable HTTP β†’ SSE β†’ plain JSON-RPC) which is custom-coded on top of the SDK's transport interfaces.

Notable Project-Specific Patterns

Custom handler signature: All tool handlers use a three-argument form:

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

The SDK's native form is two-argument. The extra state interface{} (always nil at the call site) and interface{} return value exist to support the jq middleware pipeline and DIFC write-sink logging. This is a deliberate internal convention.

registerToolWithoutValidation: Uses server.AddTool method (bypasses JSON Schema validation) rather than the sdk.AddTool function (validates input schema). This is required because backends may return draft-07 schemas that fail the SDK's validator. The distinction relies on internal SDK behavior and should be re-evaluated with each SDK upgrade.

NormalizeInputSchema: Custom pre-processor ensures backend schemas have "type": "object" and "properties": {} before registration. This defensively handles backends that omit these fields.

paginateAll generic helper: Custom Go-generics abstraction over the SDK's cursor-based list pagination for tools, resources, and prompts.

headerInjectingRoundTripper: Custom http.RoundTripper that injects auth headers into SDK-managed transports (StreamableClientTransport, SSEClientTransport), which don't expose a per-request header API.

ConvertToCallToolResult: Custom converter that handles 5 distinct backend response shapes (raw array, object with content field, no-content object, parse failures, empty content arrays).

Recent Updates

A v1.5.0-pre.1 is available. Specific release notes were not accessible in this run (external data filtered by integrity policy), but the pre-release status warrants monitoring for:

  • Schema validation configuration options
  • Transport header injection APIs
  • Pagination helper utilities

Improvement Opportunities

πŸƒ Quick Wins

1. Add a page limit guard to paginateAll

The paginateAll helper in internal/mcp/connection.go iterates cursor pages with no bound. A misbehaving or adversarial backend returning infinite pages would consume unbounded memory and time. A configurable max-pages parameter (e.g., default 100) would add resilience:

func paginateAll[T any](serverID, itemKind string, maxPages int, fetch func(cursor string) (paginatedPage[T], error)) ([]T, error) {
    // ... add page counter check
}

2. Add logger to test server

internal/testutil/mcptest/server.go uses sdk.ServerOptions{} without a logger. Adding a no-op or test-level slog logger would surface SDK-level protocol issues during integration test failures.

✨ Feature Opportunities

1. Monitor go-sdk v1.5.0 for schema bypass option

The registerToolWithoutValidation workaround relies on the distinction between sdk.AddTool (validates) and server.AddTool method (skips validation). If v1.5.0 adds an explicit SkipSchemaValidation option on sdk.Tool or sdk.ServerOptions, this workaround can be removed and the handler signature can be simplified back to the SDK native form.

2. Monitor for native header injection in transport structs

The headerInjectingRoundTripper exists because StreamableClientTransport and SSEClientTransport don't expose a headers field. If a future SDK release adds Headers map[string]string to these structs, the custom RoundTripper can be removed.

3. Monitor for SDK pagination helper

The custom generic paginateAll is clean code, but if the SDK adds a CollectAll or similar utility for its paginated list APIs, the custom implementation can be removed.

πŸ“ Best Practice Alignment

1. Re-evaluate registerToolWithoutValidation on each SDK upgrade

The server.AddTool method (as opposed to the sdk.AddTool function) is being used to bypass schema validation. This relies on an internal SDK implementation detail. It should be explicitly re-checked whenever the SDK is upgraded to ensure the behavior hasn't changed.

2. Document the three-argument handler convention

The custom func(ctx, req, state interface{}) (*CallToolResult, interface{}, error) handler type is used throughout the codebase but is an internal convention not documented at the package level. A package-level godoc comment or ARCHITECTURE.md note explaining why this differs from the SDK native signature would help future contributors.

πŸ”§ General Improvements

1. Simplify ConvertToCallToolResult with a decision table

The function in internal/mcp/tool_result.go handles 5 branches with comments explaining each case. The logic is correct and well-tested, but restructuring it with named predicates (isRawArray, hasContentField, etc.) would make the control flow easier to scan:

switch {
case isRawArray(data):         return wrapAsText(data)
case !hasContentField(data):   return wrapAsText(data)
default:                       return convertContent(data)
}

2. Make transport negotiation timeouts configurable

In internal/mcp/http_transport.go, the reconnect timeout is hardcoded at 10 seconds:

connectCtx, cancel := context.WithTimeout(c.ctx, 10*time.Second)

This could benefit from a configurable value, especially for slow-starting backends.

Recommendations

  1. [Low risk, high value] Add maxPages guard to paginateAll β€” protects against runaway backends
  2. [Low risk, medium value] Add godoc for the custom three-argument handler convention
  3. [Monitoring] Track go-sdk v1.5.0 release for schema validation options, transport header support, and pagination helpers β€” each could remove significant custom workaround code
  4. [Process] Add a checklist item to upgrade PRs: re-verify registerToolWithoutValidation behavior on each SDK version bump

Next Steps

  • Watch go-sdk releases for v1.5.0 stable
  • Consider filing an upstream issue on modelcontextprotocol/go-sdk requesting: (a) explicit schema validation bypass option, (b) headers field on transport structs

Generated by Go Fan 🐹 β€” Run Β§24023435150
Module summary saved to: specs/mods/go-sdk.md (session artifact)

Note

πŸ”’ Integrity filter blocked 77 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.8M Β· β—·

  • expires on Apr 13, 2026, 7:47 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