MCP Variants - Go Implementation
Go implementation of SEP-2053: Server Variants — a server-level variant mechanism for MCP that lets servers expose multiple tool/resource/prompt sets simultaneously, selectable per-request via _meta.
Quick Start
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:
{
"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:
{
"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/ 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:
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
const (
Stable VariantStatus = "stable"
Experimental VariantStatus = "experimental"
Deprecated VariantStatus = "deprecated"
)
DeprecationInfo
Migration guidance for deprecated variants:
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:
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
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.