experimental-ext-variants/go/sdk/variants/session.go
Kurt Degiorgio c01fd188d2
Implement variant-aware MCP server (#3)
Summary

Builds on the types and architecture from the previous PR to deliver a
working variant-aware MCP
server that routes requests to inner mcp.Server instances based on
per-request _meta variant selection,
enabling a single MCP endpoint to serve multiple tool/resource/prompt
variants simultaneously.
  
  ```go
 // Create standard mcp.Server instances — one per variant
codeReviewServer := mcp.NewServer(&mcp.Implementation{Name:
"devplatform", Version: "v1.0.0"}, nil)
mcp.AddTool(codeReviewServer, &mcp.Tool{Name: "list_pull_requests",
...}, handler)
mcp.AddTool(codeReviewServer, &mcp.Tool{Name: "get_diff", ...}, handler)

pmServer := mcp.NewServer(&mcp.Implementation{Name: "devplatform",
Version: "v1.0.0"}, nil)
  mcp.AddTool(pmServer, &mcp.Tool{Name: "list_issues", ...}, handler)
  mcp.AddTool(pmServer, &mcp.Tool{Name: "create_issue", ...}, handler)

  // Register them as variants on a single server
vs := variants.NewServer(&mcp.Implementation{Name: "devplatform",
Version: "v1.0.0"}).
      WithVariant(variants.ServerVariant{
          ID:          "code-review",
          Description: "PR and code review operations",
          Status:      variants.Stable,
      }, codeReviewServer, 0).
      WithVariant(variants.ServerVariant{
          ID:          "project-management",
          Description: "Issue and project tracking operations",
          Status:      variants.Stable,
      }, pmServer, 1)

      // Future: register variants backed by remote MCP servers
// WithHTTPVariant(v, mcpServer, priority) — variant backed by an
mcp.Server exposed over HTTP
// WithRemoteVariant(v, "https://...", priority) — variant backed by a
remote MCP endpoint

  // Serve over HTTP for multiple concurrent clients
  handler := variants.NewStreamableHTTPHandler(vs, nil)
  http.ListenAndServe(":8080", handler)

  // Or serve over stdio for single-client use:
  // vs.Run(ctx, &mcp.StdioTransport{})
```
  

  Key additions

  - Backend abstraction (inMemoryBackend) — creates in-memory transport pairs between the variant server and
   inner mcp.Server instances, with notification forwarding for progress and logging events.
  - Request dispatcher (dispatch.go) — handles routing for tools/list, tools/call, resources/list,
  resources/read, resources/subscribe, prompts/list, prompts/get, and completion/complete methods.
  Implements variant-scoped cursor wrapping to correctly paginate across multiple inner servers.
  - Session middleware (session.go) — manages per-session inner connections in stateful mode and shared
  connections in stateless HTTP mode, ensuring proper lifecycle management.
  - Initialize enrichment — injects availableVariants into the server's capabilities response so clients can
   discover which variants are available.
  - Capability discovery — merges inner server capabilities via union to present a unified capability set to
   clients.
  - NewStreamableHTTPHandler — provides an HTTP handler for multi-client serving over Streamable HTTP
  transport.
  - Integration tests (server_test.go, 458 lines) — covers end-to-end variant selection, backward
  compatibility (no variant specified), HTTP transport, and stateless modes.
  - Examples — stdio-based (examples/server/variants/main.go) and HTTP-based
  (examples/server/variants-http/main.go) example servers demonstrating the API.
  - README — rewritten with API documentation and architecture diagrams.


TODO (subsequent PRs):
- Unit Testing 
- More integration tests

Signed-off-by: Kurt Degiorgio <kdegiorgio@bloomberg.net>
2026-02-18 12:57:21 +00:00

138 lines
3.8 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"
"sync"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// ---------------------------------------------------------------------------
// Per-session types
// ---------------------------------------------------------------------------
// innerConnection holds the resources for communicating with one inner
// server. In stateful mode one is created per client session; in
// stateless mode a single instance is shared across all requests.
type innerConnection struct {
clientSession *mcp.ClientSession
cleanupFn func()
}
// close tears down the client session and invokes the backend-specific
// cleanup function.
func (c *innerConnection) close() {
c.clientSession.Close()
if c.cleanupFn != nil {
c.cleanupFn()
}
}
// sessionState holds all per-session state for one front client.
type sessionState struct {
dispatcher *dispatcher
}
// close tears down all inner connections for this session.
func (ss *sessionState) close() {
for _, c := range ss.dispatcher.connections {
c.close()
}
}
// injectVariantMeta sets the variant ID in a Params' _meta map,
// preserving any existing metadata.
func injectVariantMeta(p mcp.Params, variantID string) {
meta := p.GetMeta()
if meta == nil {
meta = map[string]any{}
p.SetMeta(meta)
}
meta[metaKeyVariant] = variantID
}
// ---------------------------------------------------------------------------
// Session lifecycle
// ---------------------------------------------------------------------------
// createSessionState sets up inner connections for all variants and returns
// the per-session state.
func (s *Server) createSessionState(ctx context.Context, frontSession *mcp.ServerSession) (*sessionState, error) {
connections := make(map[string]*innerConnection, len(s.variants))
for _, entry := range s.variants {
conn, err := entry.backend.connect(ctx, entry.variant, frontSession)
if err != nil {
for _, c := range connections {
c.close()
}
return nil, err
}
connections[entry.variant.ID] = conn
}
return &sessionState{
dispatcher: &dispatcher{
server: s,
connections: connections,
},
}, nil
}
// sessionMiddleware builds the receiving middleware that manages per-session
// state and delegates to the variant dispatcher.
//
// When shared is non-nil (stateless mode), all requests use the shared
// connections instead of creating per-session state.
func (s *Server) sessionMiddleware(sessions *sync.Map, shared *sessionState) mcp.Middleware {
return func(next mcp.MethodHandler) mcp.MethodHandler {
return func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
ss := req.GetSession().(*mcp.ServerSession)
if method == "initialize" {
// Let the SDK handle init first (capability negotiation etc.)
result, err := next(ctx, method, req)
if err != nil {
return nil, err
}
// In stateless mode, skip per-session connection creation;
// requests will use the shared connections.
if shared == nil {
state, err := s.createSessionState(ctx, ss)
if err != nil {
return nil, err
}
sessions.Store(ss, state)
// Clean up when the front session closes.
go func() {
ss.Wait()
sessions.Delete(ss)
state.close()
}()
}
// Enrich the init result with variant information
return s.enrichInitResult(ctx, result, req)
}
// Try per-session state first
if v, ok := sessions.Load(ss); ok {
state := v.(*sessionState)
return state.dispatcher.handle(ctx, method, req, next)
}
// Fall back to shared state (stateless mode)
if shared != nil {
return shared.dispatcher.handle(ctx, method, req, next)
}
return next(ctx, method, req)
}
}
}