Skip to content

Commit 4beb517

Browse files
JAORMXclaude
andcommitted
Use prefix strategy as fallback in priority resolver
Change priority resolver to use prefix strategy for backends not in the priority list instead of dropping their conflicting tools. This prevents data loss while maintaining explicit control for prioritized backends. Behavior: - Backends in priority list: priority strategy (first wins) - Backends NOT in priority list with conflicts: prefix strategy fallback - Example: priority_order=["github"], but slack+teams both have "send_message" Result: "slack_send_message" and "teams_send_message" (both included) This addresses review feedback about dropping tools unnecessarily and provides a more practical default behavior. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 5a166ca commit 4beb517

File tree

8 files changed

+2338
-11
lines changed

8 files changed

+2338
-11
lines changed

MCP_SDK_ANALYSIS.md

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
# MCP Go SDK Analysis for vMCP
2+
3+
## Executive Summary
4+
5+
**Both SDKs support standard Go `http.Handler` middleware.** However, **mark3labs/mcp-go** remains the recommended choice due to proven battle-testing in ToolHive, explicit streamable-HTTP support, and simpler integration patterns.
6+
7+
## Critical Discovery: go-sdk HTTP Middleware Support
8+
9+
The official SDK **DOES support standard HTTP middleware** through wrapper functions:
10+
11+
```go
12+
// go-sdk pattern
13+
handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server { return server }, nil)
14+
authenticatedHandler := authMiddleware(handler) // Standard middleware wrapping!
15+
http.HandleFunc("/mcp", authenticatedHandler.ServeHTTP)
16+
```
17+
18+
This was not initially clear from documentation but confirmed in `examples/server/auth-middleware/main.go`.
19+
20+
## Middleware Reusability: BOTH SDKs ✅
21+
22+
### mark3labs/mcp-go
23+
```go
24+
// Direct http.Handler interface
25+
httpServer := server.NewStreamableHTTPServer(mcpServer) // Implements http.Handler
26+
handler := authMw(authzMw(telemetryMw(httpServer))) // Standard chaining
27+
http.ListenAndServe(":8080", handler)
28+
```
29+
30+
### modelcontextprotocol/go-sdk
31+
```go
32+
// Handler wrapper pattern
33+
handler := mcp.NewStreamableHTTPHandler(getServer, nil) // Returns http.Handler
34+
wrapped := authMw(authzMw(telemetryMw(handler))) // Standard chaining
35+
http.HandleFunc("/mcp", wrapped.ServeHTTP)
36+
```
37+
38+
**Verdict**: Both work with ToolHive's existing middleware (auth, authz, telemetry, audit, mcp-parser).
39+
40+
## Feature Comparison Matrix
41+
42+
| Feature | mark3labs/mcp-go | go-sdk | Winner |
43+
|---------|------------------|---------|--------|
44+
| **HTTP middleware** | ✅ Direct `http.Handler` | ✅ Via wrapper functions | Tie |
45+
| **Streamable-HTTP** |`NewStreamableHttpClient()` explicit |`NewStreamableHTTPHandler()` explicit | Tie |
46+
| **SSE support** |`NewSSEMCPClient()` |`NewSSEHandler()` | Tie |
47+
| **ToolHive usage** |`pkg/transport/bridge.go` | ❌ None | **mark3labs** |
48+
| **Client flexibility** |`WithHTTPHeaderFunc()` dynamic headers | ⚠️ Middleware only | **mark3labs** |
49+
| **OAuth built-in** |`WithHTTPOAuth()` |`auth.RequireBearerToken()` | Tie |
50+
| **Server middleware** | ⚠️ MCP-level only | ✅ Both HTTP and MCP-level | **go-sdk** |
51+
| **Documentation** | ✅ Good examples | ⚠️ Limited (but improving) | **mark3labs** |
52+
| **Official status** | ❌ Community |**Official** | **go-sdk** |
53+
| **Battle-tested** | ✅ Production ToolHive | ❌ Unknown | **mark3labs** |
54+
55+
## Key Architectural Differences
56+
57+
### Client-Side Auth Injection
58+
59+
**mark3labs/mcp-go** (More flexible):
60+
```go
61+
client.NewStreamableHttpClient(backendURL,
62+
transport.WithHTTPHeaderFunc(func(r *http.Request) {
63+
// Per-request dynamic auth - PERFECT for vMCP per-backend auth
64+
injectAuth(r, backendID, authStrategy)
65+
}),
66+
)
67+
```
68+
69+
**go-sdk** (Less flexible):
70+
```go
71+
client.AddSendingMiddleware(func(ctx, method, req) (Result, error) {
72+
// Can modify requests, but less fine-grained HTTP control
73+
return next(ctx, method, req)
74+
})
75+
```
76+
77+
**Impact for vMCP**: mark3labs provides easier per-backend authentication since vMCP needs to inject different credentials for each backend.
78+
79+
### Server-Side Middleware
80+
81+
**mark3labs/mcp-go** (HTTP-only):
82+
```go
83+
httpServer := server.NewStreamableHTTPServer(mcpServer)
84+
handler := middleware(httpServer) // HTTP middleware only
85+
```
86+
87+
**go-sdk** (Dual-layer):
88+
```go
89+
// Layer 1: HTTP middleware (standard Go)
90+
httpHandler := mcp.NewStreamableHTTPHandler(getServer, nil)
91+
wrappedHTTP := authMiddleware(httpHandler)
92+
93+
// Layer 2: MCP protocol middleware (SDK-specific)
94+
server.AddReceivingMiddleware(func(ctx, method, req) { ... })
95+
```
96+
97+
**Impact for vMCP**: go-sdk's dual-layer is cleaner for separating HTTP concerns (auth) from MCP concerns (tool routing, aggregation).
98+
99+
## ToolHive Middleware Integration
100+
101+
All ToolHive middleware work with **both SDKs**:
102+
103+
| Middleware | Integration |
104+
|-----------|-------------|
105+
| `pkg/auth` (OIDC) | ✅ Standard `func(http.Handler) http.Handler` |
106+
| `pkg/authz` (Cedar) | ✅ Standard pattern |
107+
| `pkg/telemetry` (OTel) | ✅ Standard pattern |
108+
| `pkg/audit` | ✅ Standard pattern |
109+
| `pkg/mcp` (Parser) | ✅ Standard pattern |
110+
| `pkg/auth/tokenexchange` | ✅ Standard pattern |
111+
112+
## Recommendation: mark3labs/mcp-go
113+
114+
**Use mark3labs/mcp-go** despite go-sdk's middleware support because:
115+
116+
### Pros:
117+
1.**Battle-tested**: Already proven in `pkg/transport/bridge.go`
118+
2.**Client flexibility**: `WithHTTPHeaderFunc()` perfect for per-backend auth
119+
3.**Zero migration risk**: Continue existing patterns
120+
4.**Simpler**: Direct `http.Handler`, no wrapper layer
121+
5.**Documented**: More practical examples
122+
6.**Known quantities**: No surprises in production
123+
124+
### Cons:
125+
1.**Not official**: Potential future migration needed
126+
2.**Single-layer middleware**: Only HTTP, no MCP-level hooks
127+
128+
### go-sdk Consideration:
129+
- **Defer adoption** until vMCP v2 or if mark3labs maintenance becomes an issue
130+
- **Design for migration**: Abstract SDK behind `vmcp.BackendClient` interface
131+
- **Re-evaluate**: After go-sdk gains more production usage
132+
133+
## Implementation Paths
134+
135+
### With mark3labs/mcp-go (Recommended)
136+
137+
```go
138+
// pkg/vmcp/server/server.go
139+
func (v *VMCPServer) Start(ctx context.Context, cfg *config.Config) error {
140+
// Create MCP server
141+
mcpSrv := server.NewMCPServer("vmcp", version,
142+
server.WithToolCapabilities(true),
143+
server.WithResourceCapabilities(true, true),
144+
)
145+
146+
// Register aggregated tools
147+
for _, tool := range v.aggregatedTools {
148+
mcpSrv.AddTool(tool, v.makeToolHandler(tool))
149+
}
150+
151+
// Create streamable-HTTP handler
152+
httpHandler := server.NewStreamableHTTPServer(mcpSrv)
153+
154+
// Apply existing ToolHive middleware (NO CHANGES NEEDED)
155+
handler := v.applyMiddleware(httpHandler, cfg)
156+
157+
return http.ListenAndServe(fmt.Sprintf(":%d", cfg.Port), handler)
158+
}
159+
160+
func (v *VMCPServer) applyMiddleware(h http.Handler, cfg *config.Config) http.Handler {
161+
// Reuse existing ToolHive middleware
162+
authMw, _ := auth.GetAuthenticationMiddleware(ctx, cfg.IncomingAuth.OIDC)
163+
authzMw := authz.NewCedarAuthorizer(cfg.IncomingAuth.Authz.Policies)
164+
telemetryMw := telemetry.NewHTTPMiddleware(cfg.Telemetry, ...)
165+
166+
return authMw(authzMw.Middleware(telemetryMw(mcp.ParsingMiddleware(h))))
167+
}
168+
169+
// Client with per-backend auth
170+
func (v *VMCPServer) createBackendClient(target *vmcp.BackendTarget) (*client.Client, error) {
171+
return client.NewStreamableHttpClient(
172+
target.BaseURL,
173+
transport.WithHTTPHeaderFunc(func(r *http.Request) {
174+
// Dynamic per-backend auth injection
175+
v.auth.AuthenticateRequest(ctx, r, target.AuthStrategy, target.AuthMetadata)
176+
}),
177+
)
178+
}
179+
```
180+
181+
### With go-sdk (Alternative)
182+
183+
```go
184+
// pkg/vmcp/server/server.go
185+
func (v *VMCPServer) Start(ctx context.Context, cfg *config.Config) error {
186+
// Create MCP server with MCP-level middleware
187+
mcpSrv := mcp.NewServer(&mcp.Implementation{Name: "vmcp"}, nil)
188+
mcpSrv.AddReceivingMiddleware(v.routingMiddleware) // MCP-level routing
189+
190+
// Register aggregated tools
191+
for _, tool := range v.aggregatedTools {
192+
mcp.AddTool(mcpSrv, tool, v.makeToolHandler(tool))
193+
}
194+
195+
// Create HTTP handler with routing function
196+
httpHandler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server {
197+
return mcpSrv
198+
}, nil)
199+
200+
// Apply existing ToolHive HTTP middleware
201+
handler := v.applyMiddleware(httpHandler, cfg)
202+
203+
http.HandleFunc("/vmcp", handler.ServeHTTP)
204+
return http.ListenAndServe(fmt.Sprintf(":%d", cfg.Port), nil)
205+
}
206+
```
207+
208+
## Migration Strategy
209+
210+
1. **Phase 1**: Implement with mark3labs/mcp-go
211+
2. **Abstract**: Hide SDK behind `vmcp.BackendClient` interface
212+
3. **Monitor**: Watch go-sdk adoption in community
213+
4. **Evaluate**: Reassess after 6-12 months
214+
5. **Migrate**: If/when go-sdk proves superior
215+
216+
## Conclusion
217+
218+
**Use mark3labs/mcp-go** for pragmatic reasons (proven, flexible, simple), while designing interfaces that enable future go-sdk migration if needed.
219+
220+
Both SDKs work with ToolHive middleware, but mark3labs offers better client-side flexibility for vMCP's per-backend authentication requirements.

0 commit comments

Comments
 (0)