Skip to content

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

Description

@github-actions

🐹 Go Fan Report: modelcontextprotocol/go-sdk

Module Overview

github.com/modelcontextprotocol/go-sdk is the official Go SDK for the Model Context Protocol (MCP) β€” the foundational dependency of this entire project. It provides the server/client infrastructure, transport implementations (stdio, streamable HTTP, SSE), JSON-RPC session management, and capability negotiation. This project is essentially a thin but sophisticated layer on top of this SDK, proxying MCP between agents and backend servers.

Version: v1.4.1 (latest, rapidly evolving β€” 5 releases tracked from v1.3.0-pre.1 to v1.4.1)


Current Usage in gh-aw-mcpg

  • Files: 12 production files, all importing as sdk "github.com/modelcontextprotocol/go-sdk/mcp"
  • Import Count: 12 production + 15 test file imports
  • Key APIs Used: NewServer, NewClient, Server.AddTool, NewStreamableHTTPHandler, CommandTransport, StreamableClientTransport, SSEClientTransport, NewInMemoryTransports, ClientSession.* methods

Usage Pattern Summary

Layer Role
internal/mcp/ Client-side SDK usage β€” connecting to backend MCP servers via all 3 transports
internal/server/ Server-side SDK usage β€” creating MCP servers that clients connect to
internal/middleware/ Wrapping CallToolResult / CallToolRequest types
internal/testutil/mcptest/ Full in-memory test harness using NewInMemoryTransports

Highlights of good SDK usage:

  • βœ… Custom slog.Logger integration in all ServerOptions, ClientOptions, and StreamableHTTPOptions
  • βœ… sdk.NewInMemoryTransports() for elegant in-process test transport
  • βœ… server.AddTool method (vs sdk.AddTool function) to bypass strict schema validation β€” correctly handles backends using JSON Schema draft-07
  • βœ… Transport fallback chain: streamable HTTP β†’ SSE β†’ plain JSON-RPC
  • βœ… Session reconnect logic for expired HTTP sessions

Research Findings

Protocol Status

The MCP protocol is evolving rapidly. The go-sdk v1.4.1 supports the 2025-03-26 spec (streamable HTTP) as primary and the 2024-11-05 spec (SSE) as deprecated. The project correctly reflects this with its deprecation warning on SSE connections.

SDK Capabilities Used vs Available

Feature Available Used
stdio transport βœ… βœ…
Streamable HTTP server βœ… βœ…
Streamable HTTP client βœ… βœ…
SSE client βœ… βœ…
In-memory transport βœ… βœ…
sdk.TextContent βœ… βœ…
sdk.ImageContent βœ… ❌
sdk.EmbeddedResourceContent βœ… ❌
Tool annotations βœ… ❌ (not forwarded)
Paginated ListTools βœ… ❌ (single page only)
sdk.AddTool (typed handlers) βœ… Intentionally bypassed

Improvement Opportunities

πŸ› Bug: Image and Resource Content Types Silently Dropped

File: internal/mcp/tool_result.go β€” ConvertToCallToolResult

The content parsing struct only captures Type and Text:

Content []struct {
    Type string `json:"type"`
    Text string `json:"text,omitempty"`
} `json:"content"`

When a backend returns "image" content ({"type":"image","data":"base64...","mimeType":"image/png"}), the data and mimeType fields are not captured. The default case then creates sdk.TextContent{Text: ""} β€” an empty text item, silently discarding the image. Same issue for "resource" content.

Impact: Any backend that returns image content (e.g. screenshot tools, vision tools) will have that content silently dropped and replaced with an empty text item.

Fix: Expand the content parsing struct and add explicit cases:

Content []struct {
    Type     string                 `json:"type"`
    Text     string                 `json:"text,omitempty"`
    Data     string                 `json:"data,omitempty"`     // for image
    MIMEType string                 `json:"mimeType,omitempty"` // for image
    Resource map[string]interface{} `json:"resource,omitempty"` // for resource
} `json:"content"`

Then in the switch:

case "image":
    content = append(content, &sdk.ImageContent{
        Data:     item.Data,
        MIMEType: item.MIMEType,
    })
case "resource":
    // handle embedded resource

πŸƒ Quick Win: Pagination for tools/list, resources/list, prompts/list

File: internal/mcp/connection.go β€” listTools, listResources, listPrompts

All three list methods call the SDK exactly once with empty params. The SDK's result structs include a NextCursor field for paginated backends. With large tool sets (e.g., GitHub MCP server has 50+ tools, which could grow), a paginated backend would silently drop tools after the first page.

Fix: Implement a pagination loop:

func (c *Connection) listTools() (*Response, error) {
    if err := c.requireSession(); err != nil {
        return nil, err
    }
    var allTools []*sdk.Tool
    var cursor string
    for {
        result, err := c.getSDKSession().ListTools(c.ctx, &sdk.ListToolsParams{Cursor: cursor})
        if err != nil {
            return nil, err
        }
        allTools = append(allTools, result.Tools...)
        if result.NextCursor == "" {
            break
        }
        cursor = result.NextCursor
    }
    return marshalToResponse(&sdk.ListToolsResult{Tools: allTools})
}

πŸƒ Quick Win: Include Error Message in newErrorCallToolResult

File: internal/server/unified.go

Currently error results have IsError: true but no content, leaving MCP clients with no explanation:

func newErrorCallToolResult(err error) (*sdk.CallToolResult, interface{}, error) {
    return &sdk.CallToolResult{IsError: true}, nil, err
}

MCP spec recommends including the error description as text content. This helps MCP clients (and AI agents) understand what went wrong:

func newErrorCallToolResult(err error) (*sdk.CallToolResult, interface{}, error) {
    return &sdk.CallToolResult{
        IsError: true,
        Content: []sdk.Content{
            &sdk.TextContent{Text: err.Error()},
        },
    }, nil, err
}

✨ Feature Opportunity: Forward Tool Annotations from Backends

File: internal/server/tool_registry.go β€” registerToolsFromBackend

The MCP 2025 spec adds annotations to tools (e.g., readOnlyHint, destructiveHint, openWorldHint). When listing backend tools, annotations are present in the tools/list response but the current parsing struct does not capture them:

Tools []struct {
    Name        string                 `json:"name"`
    Description string                 `json:"description"`
    InputSchema map[string]interface{} `json:"inputSchema"`
    // Annotations missing!
} `json:"tools"`

These annotations help clients (especially AI agents) understand tool safety characteristics. Forwarding them through the gateway would improve agent safety awareness.


Recommendations (Prioritized)

  1. [BUG - High] Fix image/resource content type handling in ConvertToCallToolResult β€” backends returning non-text content silently drop data
  2. [Enhancement - Medium] Add error message text content to newErrorCallToolResult β€” improves debuggability for MCP clients
  3. [Enhancement - Medium] Implement paginated listTools/listResources/listPrompts β€” prevents silent tool loss with large backends
  4. [Enhancement - Low] Forward tool annotations from backend tools to gateway-registered tools

Next Steps

  • Fix ConvertToCallToolResult to handle "image" and "resource" content types
  • Add TestConvertToCallToolResult_ImageContent test case in internal/mcp/tool_result_test.go
  • Update newErrorCallToolResult to include error message in content
  • Implement pagination loop in listTools, listResources, listPrompts
  • Add annotations field to backend tool parsing struct

Generated by Go Fan 🐹
Module summary saved to: specs/mods/go-sdk.md (cache)
Run ID: Β§23635978953

Note

πŸ”’ Integrity filter blocked 12 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 Β· β—·

  • expires on Apr 3, 2026, 7:42 AM UTC

Metadata

Metadata

Assignees

Type

No type

Fields

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