Skip to content

Commit c01fd18

Browse files
authored
Implement variant-aware MCP server (#3)
Summary Builds on the types and architecture from the previous PR to deliver a working variant-aware MCP server that routes requests to inner mcp.Server instances based on per-request _meta variant selection, enabling a single MCP endpoint to serve multiple tool/resource/prompt variants simultaneously. ```go // Create standard mcp.Server instances — one per variant codeReviewServer := mcp.NewServer(&mcp.Implementation{Name: "devplatform", Version: "v1.0.0"}, nil) mcp.AddTool(codeReviewServer, &mcp.Tool{Name: "list_pull_requests", ...}, handler) mcp.AddTool(codeReviewServer, &mcp.Tool{Name: "get_diff", ...}, handler) pmServer := mcp.NewServer(&mcp.Implementation{Name: "devplatform", Version: "v1.0.0"}, nil) mcp.AddTool(pmServer, &mcp.Tool{Name: "list_issues", ...}, handler) mcp.AddTool(pmServer, &mcp.Tool{Name: "create_issue", ...}, handler) // Register them as variants on a single server vs := variants.NewServer(&mcp.Implementation{Name: "devplatform", Version: "v1.0.0"}). WithVariant(variants.ServerVariant{ ID: "code-review", Description: "PR and code review operations", Status: variants.Stable, }, codeReviewServer, 0). WithVariant(variants.ServerVariant{ ID: "project-management", Description: "Issue and project tracking operations", Status: variants.Stable, }, pmServer, 1) // Future: register variants backed by remote MCP servers // WithHTTPVariant(v, mcpServer, priority) — variant backed by an mcp.Server exposed over HTTP // WithRemoteVariant(v, "https://...", priority) — variant backed by a remote MCP endpoint // Serve over HTTP for multiple concurrent clients handler := variants.NewStreamableHTTPHandler(vs, nil) http.ListenAndServe(":8080", handler) // Or serve over stdio for single-client use: // vs.Run(ctx, &mcp.StdioTransport{}) ``` Key additions - Backend abstraction (inMemoryBackend) — creates in-memory transport pairs between the variant server and inner mcp.Server instances, with notification forwarding for progress and logging events. - Request dispatcher (dispatch.go) — handles routing for tools/list, tools/call, resources/list, resources/read, resources/subscribe, prompts/list, prompts/get, and completion/complete methods. Implements variant-scoped cursor wrapping to correctly paginate across multiple inner servers. - Session middleware (session.go) — manages per-session inner connections in stateful mode and shared connections in stateless HTTP mode, ensuring proper lifecycle management. - Initialize enrichment — injects availableVariants into the server's capabilities response so clients can discover which variants are available. - Capability discovery — merges inner server capabilities via union to present a unified capability set to clients. - NewStreamableHTTPHandler — provides an HTTP handler for multi-client serving over Streamable HTTP transport. - Integration tests (server_test.go, 458 lines) — covers end-to-end variant selection, backward compatibility (no variant specified), HTTP transport, and stateless modes. - Examples — stdio-based (examples/server/variants/main.go) and HTTP-based (examples/server/variants-http/main.go) example servers demonstrating the API. - README — rewritten with API documentation and architecture diagrams. TODO (subsequent PRs): - Unit Testing - More integration tests Signed-off-by: Kurt Degiorgio <[email protected]>
1 parent dbf99be commit c01fd18

15 files changed

Lines changed: 2217 additions & 245 deletions

File tree

go/sdk/README.md

Lines changed: 257 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,259 @@
11
# MCP Variants - Go Implementation
22

3-
This will contain the Go implementation of the MCP Variants based on [SEP-2053](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2053).
3+
Go implementation of [SEP-2053: Server Variants](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2053) — a server-level variant mechanism for MCP that lets servers expose multiple tool/resource/prompt sets simultaneously, selectable per-request via `_meta`.
4+
5+
## Quick Start
6+
7+
```go
8+
package main
9+
10+
import (
11+
"log"
12+
"net/http"
13+
14+
"github.com/modelcontextprotocol/go-sdk/mcp"
15+
"github.com/modelcontextprotocol/experimental-ext-variants/go/sdk/variants"
16+
)
17+
18+
func main() {
19+
// Code review variant: PR-focused tools
20+
codeReviewServer := mcp.NewServer(&mcp.Implementation{Name: "devplatform", Version: "v1.0.0"}, nil)
21+
mcp.AddTool(codeReviewServer, &mcp.Tool{
22+
Name: "list_pull_requests",
23+
Description: "List open pull requests, optionally filtered by author",
24+
}, listPullRequests)
25+
mcp.AddTool(codeReviewServer, &mcp.Tool{
26+
Name: "get_diff",
27+
Description: "Get the diff for a pull request",
28+
}, getDiff)
29+
30+
// Project management variant: issue-focused tools
31+
pmServer := mcp.NewServer(&mcp.Implementation{Name: "devplatform", Version: "v1.0.0"}, nil)
32+
mcp.AddTool(pmServer, &mcp.Tool{
33+
Name: "list_issues",
34+
Description: "List issues, optionally filtered by state and labels",
35+
}, listIssues)
36+
mcp.AddTool(pmServer, &mcp.Tool{
37+
Name: "create_issue",
38+
Description: "Create a new issue with title, body, and optional labels",
39+
}, createIssue)
40+
41+
vs := variants.NewServer(&mcp.Implementation{Name: "devplatform", Version: "v1.0.0"}).
42+
WithVariant(variants.ServerVariant{
43+
ID: "code-review",
44+
Description: "Pull request and code review operations. Includes diff viewing, review comments, approval workflows, and merge controls.",
45+
Hints: map[string]string{"domain": "code-review", "accessLevel": "read-write"},
46+
Status: variants.Stable,
47+
}, codeReviewServer, 0).
48+
WithVariant(variants.ServerVariant{
49+
ID: "project-management",
50+
Description: "Issue and project tracking operations. Includes issue CRUD, labels, milestones, assignments, and project boards.",
51+
Hints: map[string]string{"domain": "project-management", "accessLevel": "read-write"},
52+
Status: variants.Stable,
53+
}, pmServer, 1)
54+
55+
// Serve over HTTP (or use vs.Run(ctx, &mcp.StdioTransport{}) for stdio)
56+
handler := variants.NewStreamableHTTPHandler(vs, nil)
57+
log.Fatal(http.ListenAndServe(":8080", handler))
58+
}
59+
```
60+
61+
Clients select a variant per-request via `_meta`:
62+
63+
```json
64+
{
65+
"method": "tools/list",
66+
"params": {
67+
"_meta": {
68+
"io.modelcontextprotocol/server-variant": "code-review"
69+
}
70+
}
71+
}
72+
```
73+
74+
Clients that don't know about variants get the default (first-ranked) variant automatically.
75+
76+
## How It Works
77+
78+
During `initialize`, the server responds with ranked `availableVariants`:
79+
80+
```json
81+
{
82+
"capabilities": {
83+
"experimental": {
84+
"io.modelcontextprotocol/server-variants": {
85+
"availableVariants": [
86+
{
87+
"id": "code-review",
88+
"description": "Pull request and code review operations...",
89+
"hints": { "domain": "code-review", "accessLevel": "read-write" },
90+
"status": "stable"
91+
},
92+
{
93+
"id": "project-management",
94+
"description": "Issue and project tracking operations...",
95+
"hints": { "domain": "project-management", "accessLevel": "read-write" },
96+
"status": "stable"
97+
}
98+
],
99+
"moreVariantsAvailable": false
100+
}
101+
}
102+
}
103+
}
104+
```
105+
106+
Each subsequent request can target a specific variant via `_meta`. The server routes the request to the appropriate backing `mcp.Server`:
107+
108+
```
109+
Client ── transport ──▸ variants.Server ──▸ sessionMiddleware
110+
111+
┌────────────┼────────────┐
112+
│ │ │
113+
initialize route by pass through
114+
(create _meta (ping, etc.)
115+
per-session variant
116+
connections) │
117+
118+
dispatcher
119+
120+
┌────────────┬────────┴────────┬────────────┐
121+
▼ ▼ ▼ ▼
122+
code-review project-mgmt security-readonly ci-automation
123+
(mcp.Server) (mcp.Server) (mcp.Server) (mcp.Server)
124+
```
125+
126+
In **stateful mode** (default, stdio and HTTP), per-session inner connections are created during `initialize` and scoped to the client session's lifetime. In **stateless mode** (via `NewStreamableHTTPHandler` with `Stateless: true`), a single set of shared connections is created at construction and reused across all requests.
127+
128+
## Features
129+
130+
- **Variant isolation**: each variant is a full `mcp.Server` with its own tools, resources, and prompts
131+
- **Per-request selection**: variant chosen via `_meta` field, no session state needed
132+
- **Default fallback**: clients without variant support get the first-ranked variant
133+
- **Custom ranking**: provide a `RankingFunc` to rank variants based on client hints
134+
- **Cursor scoping**: pagination cursors are variant-scoped and cannot be reused across variants (per SEP-2053)
135+
- **Namespace scoping**: tool names, prompt names, and resource URIs resolve within the active variant's namespace; errors include `activeVariant` in error data
136+
- **Notification forwarding**: progress and logging notifications from inner servers are forwarded to the front client with variant metadata injected
137+
- **HTTP and stdio**: works with both `StdioTransport` and `StreamableHTTPHandler`
138+
139+
## Examples
140+
141+
See [`examples/server/`](examples/server/) for runnable examples:
142+
143+
- [`variants/`](examples/server/variants/) — stdio transport (single client)
144+
- [`variants-http/`](examples/server/variants-http/) — HTTP transport (multiple concurrent clients)
145+
146+
## API
147+
148+
### Server
149+
150+
#### `variants.NewServer(impl *mcp.Implementation) *Server`
151+
152+
Creates a new variant-aware server with no registered variants. `impl` must not be nil.
153+
154+
#### `(*Server).WithVariant(v ServerVariant, mcpServer *mcp.Server, priority int) *Server`
155+
156+
Registers a variant backed by an in-memory `mcp.Server`. `priority` determines the default ordering when no `RankingFunc` is set — lower values rank higher (0 = highest priority). Panics on duplicate variant IDs. Returns the receiver for chaining.
157+
158+
#### `(*Server).WithRanking(fn RankingFunc) *Server`
159+
160+
Sets a custom ranking function used to order variants based on client hints during initialization. If nil, variants are ordered by priority value.
161+
162+
#### `(*Server).Variants() []ServerVariant`
163+
164+
Returns a copy of all registered variants in registration order.
165+
166+
#### `(*Server).RankedVariants(ctx context.Context, hints VariantHints) []ServerVariant`
167+
168+
Returns registered variants ranked by the configured `RankingFunc` (or default priority-based ranking).
169+
170+
#### `(*Server).Run(ctx context.Context, t mcp.Transport) error`
171+
172+
Starts the server on the given transport (e.g., `&mcp.StdioTransport{}`). For multi-client HTTP support, use `NewStreamableHTTPHandler` instead.
173+
174+
#### `(*Server).Close() error`
175+
176+
Releases resources held by all registered backends.
177+
178+
#### `variants.NewStreamableHTTPHandler(vs *Server, opts *mcp.StreamableHTTPOptions) *mcp.StreamableHTTPHandler`
179+
180+
Returns an `http.Handler` for serving multiple concurrent clients over HTTP. Pass `&mcp.StreamableHTTPOptions{Stateless: true}` for stateless mode.
181+
182+
### Types
183+
184+
#### `ServerVariant`
185+
186+
Describes a selectable variant:
187+
188+
```go
189+
type ServerVariant struct {
190+
ID string `json:"id"`
191+
Description string `json:"description"`
192+
Hints map[string]string `json:"hints,omitempty"`
193+
Status VariantStatus `json:"status,omitempty"`
194+
DeprecationInfo *DeprecationInfo `json:"deprecationInfo,omitempty"`
195+
}
196+
```
197+
198+
`Priority() int` returns the priority value set during registration.
199+
200+
#### `VariantStatus`
201+
202+
```go
203+
const (
204+
Stable VariantStatus = "stable"
205+
Experimental VariantStatus = "experimental"
206+
Deprecated VariantStatus = "deprecated"
207+
)
208+
```
209+
210+
#### `DeprecationInfo`
211+
212+
Migration guidance for deprecated variants:
213+
214+
```go
215+
type DeprecationInfo struct {
216+
Message string `json:"message"`
217+
Replacement string `json:"replacement,omitempty"`
218+
RemovalDate string `json:"removalDate,omitempty"`
219+
}
220+
```
221+
222+
#### `VariantHints`
223+
224+
Client-provided hints for variant selection, sent during `initialize`:
225+
226+
```go
227+
type VariantHints struct {
228+
Description string `json:"description,omitempty"`
229+
Hints map[string]any `json:"hints,omitempty"`
230+
}
231+
```
232+
233+
#### `HintValue[T any](h VariantHints, key string) (T, bool)`
234+
235+
Generic helper to extract a typed value from a `VariantHints` map.
236+
237+
#### `RankingFunc`
238+
239+
```go
240+
type RankingFunc func(ctx context.Context, hints VariantHints, variants []ServerVariant) []ServerVariant
241+
```
242+
243+
Called during initialization to rank variants based on client hints. Must return variants sorted by relevance, most appropriate first.
244+
245+
#### Well-known hint keys
246+
247+
| Constant | Key | Example values |
248+
|---|---|---|
249+
| `HintModelFamily` | `"modelFamily"` | `"anthropic"`, `"openai"`, `"local"`, `"any"` |
250+
| `HintUseCase` | `"useCase"` | `"autonomous-agent"`, `"ide"`, `"chat"` |
251+
| `HintContextSize` | `"contextSize"` | `"compact"`, `"standard"`, `"verbose"` |
252+
| `HintRenderingCapabilities` | `"renderingCapabilities"` | `"rich"`, `"markdown"`, `"text-only"` |
253+
| `HintLanguageOptimization` | `"languageOptimization"` | `"en"`, `"multilingual"`, `"code-focused"` |
254+
255+
## Known Limitations
256+
257+
- **Default variant resolution**: When a client omits `_meta` variant selection, the server re-ranks variants with empty hints to determine the default. This may differ from the ranking returned during `initialize` (where client hints were used). Per SEP-2053, the default should be the first variant from the `initialize` response. To fix this, the per-session ranked order needs to be stored during `initialize` and reused for subsequent requests.
258+
- **List-changed notifications**: Dynamic capability changes from inner servers (tool/resource/prompt list changes) are not forwarded to front clients. The Go MCP SDK does not expose generic notification sending on `ServerSession`. In practice this is acceptable because inner servers are typically statically configured.
259+
- **HTTP and remote backends**: `WithHTTPVariant` and `WithRemoteVariant` are not yet implemented.

go/sdk/examples/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)