experimental-ext-variants/go/sdk
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Sambhav Kothari cfc05d6f5e
Some checks failed
Go CI / Linting (push) Has been cancelled
Go CI / Unit Tests (push) Has been cancelled
Go CI / Unit Tests-1 (push) Has been cancelled
Go CI / Race Detection (push) Has been cancelled
fix: merge Extensions in unionCapabilities (#9)
## Summary

`unionCapabilities` merges `Experimental` map entries from inner variant
servers but skips the `Extensions` field entirely. This means any
extension registered via `ServerCapabilities.AddExtension()` on an inner
server (e.g. `io.modelcontextprotocol/skills`) is silently dropped from
the front proxy's `InitializeResult`.

- Add the same first-wins merge logic for `Extensions` that already
exists for `Experimental`
- Bump `go-sdk` to v1.4.1 which introduced the `Extensions` field on
`ServerCapabilities`
- Add unit test for `unionCapabilities` extensions merging
- Add integration test verifying extensions survive the variant path
end-to-end

## Test plan

- [x] `TestUnionCapabilities_MergesExtensions` — verifies first-wins
merge semantics for extensions
- [x] `TestIntegration_ExtensionsAdvertised` — verifies extensions
registered on inner servers appear in the client's `InitializeResult`
- [x] All existing tests pass (27/27)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 09:17:04 +01: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 fix: merge Extensions in unionCapabilities (#9) 2026-05-29 09:17:04 +01:00
go.mod fix: merge Extensions in unionCapabilities (#9) 2026-05-29 09:17:04 +01:00
go.sum fix: merge Extensions in unionCapabilities (#9) 2026-05-29 09:17:04 +01: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.