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>
256 lines
11 KiB
Markdown
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.
|