experimental-ext-variants/go/sdk/README.md
Kurt Degiorgio e1c17e047f
Add example servers for SEP-2053 variant patterns (#4)
Add four new example servers demonstrating different SEP-2053 variant
patterns:

- model-optimized (same tools, different descriptions per LLM family)
- trading (API versioning with deprecation lifecycle)
- research (context budget management)
- Refactor the existing variants-http example into a GitHub-style server
with custom ranking.

Signed-off-by: Kurt Degiorgio <kdegiorgio@bloomberg.net>
2026-03-01 12:42:43 +05:30

256 lines
11 KiB
Markdown

# MCP Variants - Go Implementation
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`.
## Quick Start
```go
package main
import (
"log"
"net/http"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/modelcontextprotocol/experimental-ext-variants/go/sdk/variants"
)
func main() {
// Code review variant: PR-focused tools
codeReviewServer := mcp.NewServer(&mcp.Implementation{Name: "devplatform", Version: "v1.0.0"}, nil)
mcp.AddTool(codeReviewServer, &mcp.Tool{
Name: "list_pull_requests",
Description: "List open pull requests, optionally filtered by author",
}, listPullRequests)
mcp.AddTool(codeReviewServer, &mcp.Tool{
Name: "get_diff",
Description: "Get the diff for a pull request",
}, getDiff)
// Project management variant: issue-focused tools
pmServer := mcp.NewServer(&mcp.Implementation{Name: "devplatform", Version: "v1.0.0"}, nil)
mcp.AddTool(pmServer, &mcp.Tool{
Name: "list_issues",
Description: "List issues, optionally filtered by state and labels",
}, listIssues)
mcp.AddTool(pmServer, &mcp.Tool{
Name: "create_issue",
Description: "Create a new issue with title, body, and optional labels",
}, createIssue)
vs := variants.NewServer(&mcp.Implementation{Name: "devplatform", Version: "v1.0.0"}).
WithVariant(variants.ServerVariant{
ID: "code-review",
Description: "Pull request and code review operations. Includes diff viewing, review comments, approval workflows, and merge controls.",
Hints: map[string]string{"domain": "code-review", "accessLevel": "read-write"},
Status: variants.Stable,
}, codeReviewServer, 0).
WithVariant(variants.ServerVariant{
ID: "project-management",
Description: "Issue and project tracking operations. Includes issue CRUD, labels, milestones, assignments, and project boards.",
Hints: map[string]string{"domain": "project-management", "accessLevel": "read-write"},
Status: variants.Stable,
}, pmServer, 1)
// Serve over HTTP (or use vs.Run(ctx, &mcp.StdioTransport{}) for stdio)
handler := variants.NewStreamableHTTPHandler(vs, nil)
log.Fatal(http.ListenAndServe(":8080", handler))
}
```
Clients select a variant per-request via `_meta`:
```json
{
"method": "tools/list",
"params": {
"_meta": {
"io.modelcontextprotocol/server-variant": "code-review"
}
}
}
```
Clients that don't know about variants get the default (first-ranked) variant automatically.
## How It Works
During `initialize`, the server responds with ranked `availableVariants`:
```json
{
"capabilities": {
"experimental": {
"io.modelcontextprotocol/server-variants": {
"availableVariants": [
{
"id": "code-review",
"description": "Pull request and code review operations...",
"hints": { "domain": "code-review", "accessLevel": "read-write" },
"status": "stable"
},
{
"id": "project-management",
"description": "Issue and project tracking operations...",
"hints": { "domain": "project-management", "accessLevel": "read-write" },
"status": "stable"
}
],
"moreVariantsAvailable": false
}
}
}
}
```
Each subsequent request can target a specific variant via `_meta`. The server routes the request to the appropriate backing `mcp.Server`:
```
Client ── transport ──▸ variants.Server ──▸ sessionMiddleware
│
┌────────────┼────────────┐
│ │ │
initialize route by pass through
(create _meta (ping, etc.)
per-session variant
connections) │
▼
dispatcher
│
┌────────────┬────────┴────────┬────────────┐
▼ ▼ ▼ ▼
code-review project-mgmt security-readonly ci-automation
(mcp.Server) (mcp.Server) (mcp.Server) (mcp.Server)
```
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.
## Features
- **Variant isolation**: each variant is a full `mcp.Server` with its own tools, resources, and prompts
- **Per-request selection**: variant chosen via `_meta` field, no session state needed
- **Default fallback**: clients without variant support get the first-ranked variant
- **Custom ranking**: provide a `RankingFunc` to rank variants based on client hints
- **Cursor scoping**: pagination cursors are variant-scoped and cannot be reused across variants (per SEP-2053)
- **Namespace scoping**: tool names, prompt names, and resource URIs resolve within the active variant's namespace; errors include `activeVariant` in error data
- **Notification forwarding**: progress and logging notifications from inner servers are forwarded to the front client with variant metadata injected
- **HTTP and stdio**: works with both `StdioTransport` and `StreamableHTTPHandler`
## Examples
See [`examples/server/`](examples/server/) for runnable examples.
## API
### Server
#### `variants.NewServer(impl *mcp.Implementation) *Server`
Creates a new variant-aware server with no registered variants. `impl` must not be nil.
#### `(*Server).WithVariant(v ServerVariant, mcpServer *mcp.Server, priority int) *Server`
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.
#### `(*Server).WithRanking(fn RankingFunc) *Server`
Sets a custom ranking function used to order variants based on client hints during initialization. If nil, variants are ordered by priority value.
#### `(*Server).Variants() []ServerVariant`
Returns a copy of all registered variants in registration order.
#### `(*Server).RankedVariants(ctx context.Context, hints VariantHints) []ServerVariant`
Returns registered variants ranked by the configured `RankingFunc` (or default priority-based ranking).
#### `(*Server).Run(ctx context.Context, t mcp.Transport) error`
Starts the server on the given transport (e.g., `&mcp.StdioTransport{}`). For multi-client HTTP support, use `NewStreamableHTTPHandler` instead.
#### `(*Server).Close() error`
Releases resources held by all registered backends.
#### `variants.NewStreamableHTTPHandler(vs *Server, opts *mcp.StreamableHTTPOptions) *mcp.StreamableHTTPHandler`
Returns an `http.Handler` for serving multiple concurrent clients over HTTP. Pass `&mcp.StreamableHTTPOptions{Stateless: true}` for stateless mode.
### Types
#### `ServerVariant`
Describes a selectable variant:
```go
type ServerVariant struct {
ID string `json:"id"`
Description string `json:"description"`
Hints map[string]string `json:"hints,omitempty"`
Status VariantStatus `json:"status,omitempty"`
DeprecationInfo *DeprecationInfo `json:"deprecationInfo,omitempty"`
}
```
`Priority() int` returns the priority value set during registration.
#### `VariantStatus`
```go
const (
Stable VariantStatus = "stable"
Experimental VariantStatus = "experimental"
Deprecated VariantStatus = "deprecated"
)
```
#### `DeprecationInfo`
Migration guidance for deprecated variants:
```go
type DeprecationInfo struct {
Message string `json:"message"`
Replacement string `json:"replacement,omitempty"`
RemovalDate string `json:"removalDate,omitempty"`
}
```
#### `VariantHints`
Client-provided hints for variant selection, sent during `initialize`:
```go
type VariantHints struct {
Description string `json:"description,omitempty"`
Hints map[string]any `json:"hints,omitempty"`
}
```
#### `HintValue[T any](h VariantHints, key string) (T, bool)`
Generic helper to extract a typed value from a `VariantHints` map.
#### `RankingFunc`
```go
type RankingFunc func(ctx context.Context, hints VariantHints, variants []ServerVariant) []ServerVariant
```
Called during initialization to rank variants based on client hints. Must return variants sorted by relevance, most appropriate first.
#### Well-known hint keys
| Constant | Key | Example values |
|---|---|---|
| `HintModelFamily` | `"modelFamily"` | `"anthropic"`, `"openai"`, `"local"`, `"any"` |
| `HintUseCase` | `"useCase"` | `"autonomous-agent"`, `"ide"`, `"chat"` |
| `HintContextSize` | `"contextSize"` | `"compact"`, `"standard"`, `"verbose"` |
| `HintRenderingCapabilities` | `"renderingCapabilities"` | `"rich"`, `"markdown"`, `"text-only"` |
| `HintLanguageOptimization` | `"languageOptimization"` | `"en"`, `"multilingual"`, `"code-focused"` |
## Known Limitations
- **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.
- **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.
- **HTTP and remote backends**: `WithHTTPVariant` and `WithRemoteVariant` are not yet implemented.