experimental-ext-variants/go/sdk
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Steven Chen e5e8f117e8
variants: redirect inner server sending methods to fix dropping notifications in stateless mode (#7)
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)
2026-03-23 15:39:57 +00:00
..
examples/server variants: redirect inner server sending methods to fix dropping notifications in stateless mode (#7) 2026-03-23 15:39:57 +00:00
variants variants: redirect inner server sending methods to fix dropping notifications in stateless mode (#7) 2026-03-23 15:39:57 +00:00
go.mod Implement variant-aware MCP server (#3) 2026-02-18 12:57:21 +00:00
go.sum Implement variant-aware MCP server (#3) 2026-02-18 12:57:21 +00:00
README.md Add example servers for SEP-2053 variant patterns (#4) 2026-03-01 12:42:43 +05:30

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.