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>
223 lines
9.4 KiB
Go
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")
|
|
}
|