Simplify variant dispatch and session by replacing per-method boilerplate with generic handlers backed by reflect. ## Motivation and Context The dispatcher and backend session had separate handler functions for every MCP method (ListTools, ListResources, CallTool, Subscribe, etc.), each following an identical pattern: extract params, inject metadata, call inner handler, cast result. This meant ~250 lines of near-identical code across dispatch.go and session.go, and every new MCP method required adding another copy. ## How Has This Been Tested? All existing unit and integration tests pass (go test -v ./...) ## Breaking Changes None. All changes are internal to the variants package. Public API is unchanged. ## Types of changes Refactor (non-breaking change which fixes an issue)
182 lines
5.8 KiB
Go
182 lines
5.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"
|
|
"errors"
|
|
"reflect"
|
|
"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 {
|
|
backendSession *backendSession
|
|
cleanupFn func()
|
|
}
|
|
|
|
// close invokes the backend-specific cleanup function which tears down
|
|
// both the client and server sessions.
|
|
func (c *innerConnection) close() {
|
|
if c.cleanupFn != nil {
|
|
c.cleanupFn()
|
|
}
|
|
}
|
|
|
|
// backendSession bypasses the in-memory transport and calls the inner
|
|
// server's handler chain directly, preserving the caller's
|
|
// context.Context values. It mirrors the mcp.ClientSession API so that
|
|
// dispatch code reads naturally.
|
|
type backendSession struct {
|
|
variantID string
|
|
serverSession *mcp.ServerSession
|
|
mcpMethodHandler mcp.MethodHandler
|
|
}
|
|
|
|
// handleReceive invokes mcpMethodHandler for any MCP method by modifying the request's
|
|
// Session field to point to the inner server session. This replaces the explicit
|
|
// per-method functions (ListTools, CallTool, etc.) with a single generic handler.
|
|
//
|
|
// The dispatcher is responsible for modifying params (metadata injection,
|
|
// cursor unwrapping) before calling this method.
|
|
func (s *backendSession) handleReceive(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
|
|
// Shallow-copy the concrete request struct so we don't mutate the caller's object
|
|
// while replacing the Session field with our inner server session.
|
|
// We can't use a wrapper (like the sending side's sessionSwappedRequest) because
|
|
// the SDK's receiving handler does type assertions on the concrete request type.
|
|
reqVal := reflect.ValueOf(req)
|
|
if reqVal.Kind() != reflect.Ptr {
|
|
return nil, errors.New("variants: expected pointer to request struct")
|
|
}
|
|
|
|
// Allocate a new struct of the same concrete type and shallow-copy all fields.
|
|
copyPtr := reflect.New(reqVal.Elem().Type())
|
|
copyPtr.Elem().Set(reqVal.Elem())
|
|
|
|
// Set Session on the copy to point to the inner server session.
|
|
sessionField := copyPtr.Elem().FieldByName("Session")
|
|
if !sessionField.IsValid() || !sessionField.CanSet() {
|
|
return nil, errors.New("variants: request type missing settable Session field")
|
|
}
|
|
sessionField.Set(reflect.ValueOf(s.serverSession))
|
|
|
|
return s.mcpMethodHandler(ctx, method, copyPtr.Interface().(mcp.Request))
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
}
|
|
|
|
// frontSessionKeyType is the context key for the front-facing ServerSession.
|
|
type frontSessionKeyType struct{}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}
|