experimental-ext-variants/go/sdk/variants/server.go
Kurt Degiorgio dbf99bebf0
Define types and architecture for a variant-aware MCP server (#2)
Defines the core types and API for server variants extension, laying
groundwork for multi-variant server implementation. Server.Run() is not
  yet implemented but architecture is designed and documented.

  - Add Server type with variant registration 
  - Implement default priority-based ranking 
  - Define ServerVariant, VariantHints, and related types per SEP spec
  - Add comprehensive example demonstrating intended multi-variant usage
  - Document implementation plan for Server.Run() dispatch architecture
  
  
  Next PRs:
  
  -  Unit Tests
  -  Implementation for Server.Run
  -  Conformance Testing

Signed-off-by: Kurt Degiorgio <kdegiorgio@bloomberg.net>
2026-02-11 23:36:11 +00:00

223 lines
9.4 KiB
Go

// Copyright 2025 The MCP Variants Authors. All rights reserved.
// Use of this source code is governed by a Apache-2.0
// license that can be found in the LICENSE file.
package variants
import (
"context"
"errors"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// variantEntry binds a ServerVariant to its backing mcp.Server.
type variantEntry struct {
variant ServerVariant
server *mcp.Server
}
// Server is a variant-aware MCP server that multiplexes across multiple
// mcp.Server instances, each associated with a ServerVariant. During
// initialization, clients provide VariantHints and the server responds with a
// ranked list of available variants. Per-request variant selection is carried
// in the _meta field.
type Server struct {
variants []variantEntry
variantMap map[string]*mcp.Server // ID -> server for O(1) lookup
rankingFunc RankingFunc
}
// NewServer creates a new variant-aware server with no registered variants.
func NewServer() *Server {
return &Server{
variantMap: make(map[string]*mcp.Server),
}
}
// WithVariant registers a ServerVariant backed by the given mcp.Server.
// priority determines the default ordering when no RankingFunc is set;
// lower values indicate higher importance (0 = highest priority). By
// default, the variant with the lowest priority value will appear first
// in the list and serve as the recommended default for clients. This
// behavior can be overridden by providing a custom RankingFunc.
//
// Per the SEP, the first variant in the ranked list MUST have status
// "stable" (unless the client explicitly requested experimental variants
// via variantHints). Priority ordering is respected within that
// constraint: variants are sorted by priority, but the first stable
// variant is always promoted to the front of the list.
//
// Variant IDs must be unique; registering a duplicate panics.
func (s *Server) WithVariant(v ServerVariant, mcpServer *mcp.Server, priority int) *Server {
if _, exists := s.variantMap[v.ID]; exists {
panic("variants: duplicate variant ID: " + v.ID)
}
v.priority = priority
s.variants = append(s.variants, variantEntry{variant: v, server: mcpServer})
s.variantMap[v.ID] = mcpServer
return s
}
// WithRanking sets a custom ranking function used to order variants based
// on client hints during initialization. The function should return variants
// sorted by relevance, with the most appropriate variant first. If nil,
// variants are ordered by their priority value (lowest first).
//
// The ranking function does not need to enforce the SEP's stable-first
// invariant. After the function returns, the SDK will automatically promote
// the first stable variant to the front of the list (per the SEP: "The first
// element MUST have status stable unless the client explicitly requested
// experimental variants via variantHints"). The rest of the ranking order
// is preserved.
//
// Returns the receiver for chaining.
func (s *Server) WithRanking(fn RankingFunc) *Server {
s.rankingFunc = fn
return s
}
// Variants returns a copy of all registered ServerVariant values in
// registration order.
func (s *Server) Variants() []ServerVariant {
out := make([]ServerVariant, len(s.variants))
for i, e := range s.variants {
out[i] = e.variant
}
return out
}
// RankedVariants returns the registered variants ranked according to the
// configured RankingFunc (or the default priority-based ranking if none is
// set).
func (s *Server) RankedVariants(ctx context.Context, hints VariantHints) []ServerVariant {
all := s.Variants()
if len(all) == 0 {
return all
}
rankFn := s.rankingFunc
if rankFn == nil {
rankFn = defaultRankingFunc
}
return rankFn(ctx, hints, all)
}
// ServerFor returns the mcp.Server associated with the given variant ID, or
// nil if no variant with that ID is registered.
func (s *Server) ServerFor(id string) *mcp.Server {
return s.variantMap[id]
}
// Run starts the variant server.
func (s *Server) Run(ctx context.Context, t mcp.Transport) error {
// TODO: Implement Server.Run using a method-dispatching proxy pattern.
//
// Architecture: the variant Server acts as a "front" MCP server that owns
// the real transport, handles lifecycle/discovery methods itself, and
// dispatches tool/resource/prompt calls to the correct inner *mcp.Server
// based on _meta["io.modelcontextprotocol/server-variant"].
//
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
//
// Extension ID (plural, for capability negotiation):
// "io.modelcontextprotocol/server-variants"
//
// Per-request _meta key (singular, for variant selection):
// "io.modelcontextprotocol/server-variant"
//
// ---------------------------------------------------------------------------
// Assumptions (v1)
// ---------------------------------------------------------------------------
//
// We need to make sure the SEP's session stability invariant is satisfied:
//
// "The server MUST ensure that the same variant is recommended and selected for the entire session, unless the client explicitly requests otherwise via variantHints."
//
// For the first version we will relax this constraint to avoid maintaining state.
//
// ---------------------------------------------------------------------------
// Steps
// ---------------------------------------------------------------------------
//
// 1. Create a "front" *mcp.Server inside Run. Configure it with
// ServerOptions.Capabilities set to the union of all variants'
// capability flags. For example, if any variant has tools, set
// Capabilities.Tools = &ToolCapabilities{ListChanged: true}. This
// causes the SDK to advertise the capability during initialize
// without registering any actual tools, resources, or prompts.
// The inner *mcp.Server instances are never connected to a
// transport — they are only used as handler registries.
//
// 2. Add a receiving middleware via AddReceivingMiddleware on the
// front server. This single middleware handles all variant-aware
// dispatch:
//
// a. "initialize": read variantHints from the client's extension
// payload at capabilities.extensions[
// "io.modelcontextprotocol/server-variants"]. Call
// RankedVariants() and call next() to let the SDK produce
// the base InitializeResult, then inject availableVariants
// (including id, description, hints, status, and
// deprecationInfo where applicable) into the response at
// capabilities.extensions[
// "io.modelcontextprotocol/server-variants"]. Include
// moreVariantsAvailable if only a subset is returned.
//
// b. "tools/list", "resources/list", "prompts/list",
// "resources/templates/list", "completion/complete":
// read _meta["io.modelcontextprotocol/server-variant"] to
// determine the active variant (default to first-ranked if
// absent). Look up the corresponding inner *mcp.Server and
// forward the request to its handler. Return only that
// variant's items. Cursors returned in paginated responses
// MUST be variant-scoped — embed the variant ID so that
// cross-variant cursor reuse is detectable.
//
// c. "tools/call", "resources/read", "prompts/get":
// read _meta["io.modelcontextprotocol/server-variant"] from
// the request. Validate the variant ID:
// - If the ID is not in the session's availableVariants,
// return JSON-RPC error -32602 with message
// "Invalid server variant" and data containing
// { requestedVariant, availableVariants } (scoped to
// the authenticated principal's visible variants).
// - If absent, default to the first-ranked variant.
// Resolve the inner *mcp.Server via ServerFor(id) and
// forward the request.
//
// d. "resources/subscribe": read the variant from _meta,
// validate as in (c). Resource notifications for this subscription
// MUST carry _meta["io.modelcontextprotocol/server-variant"]
// matching the bound variant. If the resource disappears
// from the variant, send a resource_updated notification
// followed by list_changed.
//
// e. "notifications/tools/list_changed",
// "notifications/prompts/list_changed",
// "notifications/resources/list_changed": when an inner
// server emits a list_changed notification, the middleware
// re-emits it on the front server with
// _meta["io.modelcontextprotocol/server-variant"] set to
// the originating variant's ID.
//
// f. All other methods ("ping", etc.):
// pass through to the front server's default handler.
//
// 3. Call frontServer.Run(ctx, t) to own the transport and manage
// the session lifecycle. The front server handles the JSON-RPC
// connection, SSE streaming, progress notifications, and
// session teardown.
//
// ---------------------------------------------------------------------------
// Backward compatibility
// ---------------------------------------------------------------------------
//
// If the client does not send the extension payload during
// initialize, the server omits availableVariants from the response
// and uses the first-ranked variant for all requests. The server
// behaves identically to a non-variant-aware server.
//
return errors.New("variants: Run not implemented")
}