-
Notifications
You must be signed in to change notification settings - Fork 3
Description
🐹 Go Fan Report: MCP Go SDK
Module Overview
github.com/modelcontextprotocol/go-sdk is the official Go SDK for the Model Context Protocol (MCP), maintained by Anthropic in collaboration with Google. It provides comprehensive support for building MCP clients and servers with multiple transport protocols.
Repository: https://github.com/modelcontextprotocol/go-sdk
Current Version: v1.1.0
Latest Version: v1.2.0 (December 2025)
Last Repository Update: 2026-01-22 ⚡ (yesterday!)
Current Usage in gh-aw-mcpg
Usage Statistics
- Files: 3 primary implementation files, plus testing infrastructure
- Import Count: 22+ usages of SDK APIs
- Key APIs Used: Client/Server creation, Transport abstractions, Tool registration, Session management
Implementation Patterns
1. Client Implementation (internal/mcp/connection.go)
Uses the SDK for all MCP client operations:
import sdk "github.com/modelcontextprotocol/go-sdk/mcp"
client := sdk.NewClient(&sdk.Implementation{...}, nil)
session, err := client.Connect(ctx, transport, nil)Supports three transport types:
sdk.CommandTransport- stdio-based serverssdk.StreamableClientTransport- HTTP (2025-03-26 spec)sdk.SSEClientTransport- SSE (2024-11-05 spec)
2. Server Implementation (internal/server/)
Unified Mode (1 server):
server := sdk.NewServer(&sdk.Implementation{Name: "awmg", Version: "1.0.0"}, nil)
// All tools registered with backend prefixesRouted Mode (N servers - KEY FINDING 🎯):
func createFilteredServer(unifiedServer *UnifiedServer, backendID string) *sdk.Server {
// Creates NEW server instance per backend/session
server := sdk.NewServer(&sdk.Implementation{
Name: fmt.Sprintf("awmg-%s", backendID),
Version: "1.0.0",
}, nil)
// Re-registers tools for this backend
for _, toolInfo := range tools {
server.AddTool(&sdk.Tool{
Name: toolInfo.Name,
Description: toolInfo.Description,
InputSchema: toolInfo.InputSchema,
}, wrappedHandler)
}
return server
}This is exactly the stateless server pattern that was suffering from 70-80% performance degradation without schema caching!
Research Findings
Recent Updates (Last 7 Days)
🚨 CRITICAL: Schema Caching Performance Fix (2026-01-21)
PR #685 added automatic schema caching that dramatically improves performance for stateless server patterns.
The Problem:
In stateless deployments (like gh-aw-mcpg's routed mode), creating new sdk.Server instances and calling AddTool repeatedly was causing:
- Expensive
jsonschema.ForType()reflection on every call - Schema re-parsing and re-resolution on every request
- 70-80% latency regression
- 3.4x more memory allocations
The Solution:
Global concurrent-safe schema cache using sync.Map:
- Caches schemas by
reflect.Typefor typed handlers - Caches resolved schemas by pointer for pre-defined schemas
- Zero code changes required - automatic benefit
Benchmark Results:
| Metric | Without Cache | With Cache | Improvement |
|---|---|---|---|
| Time | 161,463 ns/op | 1,223 ns/op | 132x faster ⚡ |
| Allocations | 1,072 allocs | 21 allocs | 51x fewer 🎯 |
| Memory | 39,262 B/op | 1,208 B/op | 32x less 💾 |
Real-World Results (github-mcp-server with 130 tools):
initialize: 20.47ms → 10.36ms (50% faster)tools/list: 22.98ms → 14.15ms (38% faster)- Memory allocations: 1208.70 MB → baseline (3.4x reduction)
🔧 HTTP Compliance Fix (2026-01-22)
PR #757 added Allow header to 405 responses per RFC 9110 §15.5.6:
- Fixes issues with strict HTTP gateways (like Apigee) returning 502 Bad Gateway errors
- Ensures all 405 Method Not Allowed responses include proper
Allowheader - Zero code changes required
Other Notable Improvements in v1.2.0
- Exported GetError/SetError (#753) - Better error handling API
- DisableListening option (#729) - Control standalone SSE streams
- Content-Type validation (#736) - Prevent infinite redirect loops
- Capability configuration (#713) - Fine-grained control over capabilities
- Debounce notifications (#717) - Batch server change notifications
Best Practices
From the SDK repository and recent changes:
- Stateless Server Pattern: Create new
sdk.Serverper request and rely on schema caching (now efficient!) - Transport Selection: Use
StreamableClientTransportfor modern HTTP,SSEClientTransportfor compatibility - Error Handling: Use typed errors from
jsonrpcpackage, leverageGetError/SetErrorAPIs - Capabilities: Configure via
ServerOptions.CapabilitiesandClientOptions.Capabilities - Tool Registration: Use
sdk.AddTool()for typed handlers with automatic schema inference
Improvement Opportunities
🏃 Quick Wins
1. ⚡ CRITICAL: Upgrade to v1.2.0 Immediately
Impact: HIGH - 50% faster initialization, 38% faster tool listing, 3.4x less memory
Effort: LOW - Single line change in go.mod
Risk: LOW - SDK maintains backward compatibility
Why This Matters for gh-aw-mcpg:
The routed mode implementation in internal/server/routed.go creates new sdk.Server instances dynamically:
- 1 server instance per backend per session
- Each server re-registers all tools with
AddTool() - Without schema caching: expensive reflection on every server creation
- With schema caching: cached lookups after first registration
Expected Impact:
- Faster client connections in routed mode
- Reduced memory pressure under concurrent load
- Better scalability with many backends/sessions
- Improved latency for all MCP operations
Action:
# Update go.mod
go get github.com/modelcontextprotocol/go-sdk@v1.2.0
go mod tidy
# Test both modes
make test-all2. 🔐 Use HTTP 405 Compliance Fixes
Impact: Prevents potential 502 errors from strict HTTP gateways
Effort: Zero - automatic with v1.2.0 upgrade
Risk: None
The RFC 9110 compliance ensures the gateway works correctly with enterprise proxies and API gateways like Apigee that enforce strict HTTP standards.
✨ Feature Opportunities
1. Leverage DisableListening Option
Benefit: Reduce resource usage for request-response patterns
transport := &sdk.StreamableClientTransport{
URL: url,
HTTPClient: httpClient,
DisableListening: true, // NEW: Disable standalone SSE if not needed
}Use Case: When MCP servers don't send server-initiated notifications (like tool/prompt updates), this eliminates the persistent SSE connection overhead.
Consideration: Analyze which backend servers actually send notifications vs. pure request-response.
2. Use Capability Configuration API
Benefit: Fine-grained control over server capabilities and notifications
server := sdk.NewServer(&sdk.Implementation{...}, &sdk.ServerOptions{
Capabilities: &sdk.ServerCapabilities{
ListChanged: false, // Suppress change notifications if not needed
},
})Use Case: In routed mode where servers are ephemeral per session, suppressing ListChanged notifications may reduce overhead.
3. Export GetError/SetError for Better Error Handling
Benefit: More idiomatic error handling with SDK types
// New in v1.2.0
result, err := session.CallTool(ctx, params)
if wireErr := sdk.GetError(err); wireErr != nil {
log.Printf("MCP error code: %d, message: %s", wireErr.Code, wireErr.Message)
}Use Case: Improve error logging and debugging by extracting structured error information.
📐 Best Practice Alignment
1. Review Schema Normalization Logic
Current: Custom NormalizeInputSchema() in internal/mcp/types.go
Consideration: With v1.2.0's schema caching, test if custom normalization is still necessary
The SDK's schema handling has improved significantly. Consider:
- Testing if SDK handles missing schemas gracefully with v1.2.0
- Simplifying or removing custom normalization if SDK covers all cases
- Moving normalization into tool registration if still needed
2. Optimize Routed Mode Server Caching
Current: Cache servers by backend/session key
With v1.2.0: Server creation is much cheaper (132x faster)
Consider:
- Keep caching: Still beneficial for maintaining per-session state
- Simplify logic: Less critical with new schema cache performance
- Monitor metrics: Measure actual benefit of server-level caching
3. Document Transport Selection Strategy
Current: Uses StreamableClientTransport by default
Opportunity: Document when to use each transport type
With three transport options, document the rationale:
- StreamableHTTP: Modern (2025-03-26), stateful, best performance
- SSE: Compatible (2024-11-05), server-initiated messages
- Stdio: Local processes only
🔧 General Improvements
1. Compare Unified vs Routed Mode Performance
With v1.2.0, the performance characteristics have changed:
- Unified Mode: 1 server at startup → minimal benefit (one-time cost)
- Routed Mode: N servers dynamically → major benefit (repeated cost eliminated)
Action: Benchmark both modes with v1.2.0 to quantify the improvement.
2. Add Performance Metrics
Monitor:
- Server creation time (routed mode)
- Tool registration time
- Memory allocation patterns
- Request latency percentiles
This validates the expected improvements from v1.2.0.
Recommendations
✅ Priority 1: Upgrade to v1.2.0 IMMEDIATELY
Impact: High performance improvement for routed mode
Risk: Low - backward compatible
Action: Update go.mod, run full test suite, benchmark both modes
🔍 Priority 2: Test DisableListening Option
Impact: Potential resource savings
Risk: Low - feature flag
Action: Experiment with backends that don't send notifications
📊 Priority 3: Monitor Real-World Performance
Impact: Validate improvements
Risk: None
Action: Add metrics for server creation, tool registration, memory usage
🧹 Priority 4: Review Schema Normalization
Impact: Code simplification
Risk: Low - has test coverage
Action: Test if custom normalization is still needed with v1.2.0
Next Steps
-
Immediate (This Week):
- Upgrade to v1.2.0
- Run
make test-allto validate - Benchmark routed mode performance
-
Short-term (Next Sprint):
- Add performance metrics
- Test DisableListening option
- Measure memory allocation improvements
-
Medium-term (Next Month):
- Review schema normalization necessity
- Experiment with capability configuration APIs
- Document transport selection strategy
-
Long-term (Ongoing):
- Monitor SDK releases for new features
- Track MCP spec updates
- Optimize based on real-world metrics
References:
Analysis saved to: /tmp/gh-aw/cache-memory/go-sdk-review-2026-01-23.md
AI generated by Go Fan