| Filename | Latest commit message | Latest commit date |
|---|---|---|
Registers a sending middleware on each inner server that intercepts all outgoing messages and routes them through the front session using a session-swapped request wrapper. Replaces the previous proxy client handler approach which dropped log messages in stateless mode. ## Motivation and Context Tool handlers in variant servers use req.Session (the inner session) to send notifications. Previously, log messages were silently dropped in stateless mode because the inner session is not aware of the front session. This change injects the sendingMethodHandler from the frontSession into the context and adds a sending middleware to redirect all sending requests to the frontSession handler directly. A few considerations in this implementation: * Having frontSession in the context allows notifications to be sent to the correct frontSession, when the same backend session could be used by multiple front sessions. * This solution will work for synchronous notifications that happen within the same context (progress, log, sampling, elicitation) but not working for asynchronous notifications (tool list changed, resource updated etc), because asynchronous notifications are sent with a new context. * All the operations before the sending method will still happen in the backend. Most notable impact is in stateless mode, requests from one client could change the log level for other clients. For this reason, I default the logging level to debug for now. * This implementation relies on the MCP Go SDK sendingMethodHandler taking a generic `Request` not one that tied to specific function. Based on the current SDK implementation this is unlikely to change. * The same generic method forwarding mechanism could work for receiving methods too, but with some caveat. That can be addressed in separate PR. ## How Has This Been Tested? TestNotificationDelivery — verifies progress and log messages arrive at the front clients with multiple clients making concurrent request. New tests added for context preservation that was added in https://github.com/modelcontextprotocol/experimental-ext-variants/pull/6. Context preservation is required to make the notification forwarding work anyway. Since we use the context to carry frontSession handler to the backend session. All existing tests pass ## Breaking Changes None. Tool handlers continue to use req.Session normally. ## Types of changes Bug fix (non-breaking change which fixes an issue) |
||
| .. | ||
| examples/server | ||
| variants | ||
| go.mod | ||
| go.sum | ||
| README.md | ||
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.Serverwith its own tools, resources, and prompts - Per-request selection: variant chosen via
_metafield, no session state needed - Default fallback: clients without variant support get the first-ranked variant
- Custom ranking: provide a
RankingFuncto 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
activeVariantin 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
StdioTransportandStreamableHTTPHandler
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
_metavariant selection, the server re-ranks variants with empty hints to determine the default. This may differ from the ranking returned duringinitialize(where client hints were used). Per SEP-2053, the default should be the first variant from theinitializeresponse. To fix this, the per-session ranked order needs to be stored duringinitializeand 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:
WithHTTPVariantandWithRemoteVariantare not yet implemented.