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>
126 lines
4.3 KiB
Go
126 lines
4.3 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"
|
|
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
// backend abstracts how a variant connects to its backing MCP server.
|
|
// The in-memory implementation lives in this file; future implementations
|
|
// (HTTP, remote) will satisfy the same interface.
|
|
type backend interface {
|
|
// connect creates a connection to the backing server and returns an
|
|
// innerConnection for dispatching requests. frontSession may be nil
|
|
// in stateless mode.
|
|
connect(ctx context.Context, variant ServerVariant, frontSession *mcp.ServerSession) (*innerConnection, error)
|
|
|
|
// capabilities performs an ephemeral connect to discover the server's
|
|
// advertised capabilities, then tears down the probe connection.
|
|
capabilities(ctx context.Context) (*mcp.ServerCapabilities, error)
|
|
|
|
// close releases any resources held by the backend.
|
|
close() error
|
|
}
|
|
|
|
// inMemoryBackend connects to a co-located *mcp.Server via in-memory
|
|
// transports.
|
|
//
|
|
// Limitation: list-changed and resource-updated notifications from
|
|
// inner servers are silently dropped. The mcp.ServerSession API only
|
|
// exposes NotifyProgress and Log; there is no public method to send
|
|
// tool/resource/prompt list-changed or resource-updated notifications
|
|
// to the front client. This means that if an inner server dynamically
|
|
// adds or removes tools/resources/prompts at runtime, the front
|
|
// client will NOT be notified.
|
|
//
|
|
// In practice this is acceptable because inner servers are typically
|
|
// statically configured (tools registered at startup). If dynamic
|
|
// capability changes are needed, this will require the Go MCP SDK to
|
|
// expose generic notification sending on ServerSession.
|
|
type inMemoryBackend struct {
|
|
server *mcp.Server
|
|
}
|
|
|
|
// connect creates an in-memory transport pair, connects the inner server,
|
|
// and creates a proxy client to communicate with it. Notifications for
|
|
// progress and logging are forwarded directly to the front session with
|
|
// _meta variant ID injection.
|
|
func (b *inMemoryBackend) connect(ctx context.Context, variant ServerVariant, frontSession *mcp.ServerSession) (*innerConnection, error) {
|
|
serverTransport, clientSideTransport := mcp.NewInMemoryTransports()
|
|
|
|
serverSession, err := b.server.Connect(ctx, serverTransport, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
client := mcp.NewClient(&mcp.Implementation{
|
|
Name: "variant-proxy-client",
|
|
Version: "1.0.0",
|
|
}, &mcp.ClientOptions{
|
|
ProgressNotificationHandler: func(ctx context.Context, req *mcp.ProgressNotificationClientRequest) {
|
|
if frontSession != nil {
|
|
injectVariantMeta(req.Params, variant.ID)
|
|
_ = frontSession.NotifyProgress(ctx, req.Params)
|
|
}
|
|
},
|
|
LoggingMessageHandler: func(ctx context.Context, req *mcp.LoggingMessageRequest) {
|
|
if frontSession != nil {
|
|
injectVariantMeta(req.Params, variant.ID)
|
|
_ = frontSession.Log(ctx, req.Params)
|
|
}
|
|
},
|
|
ToolListChangedHandler: func(context.Context, *mcp.ToolListChangedRequest) {},
|
|
ResourceListChangedHandler: func(context.Context, *mcp.ResourceListChangedRequest) {},
|
|
PromptListChangedHandler: func(context.Context, *mcp.PromptListChangedRequest) {},
|
|
ResourceUpdatedHandler: func(context.Context, *mcp.ResourceUpdatedNotificationRequest) {},
|
|
})
|
|
|
|
clientSession, err := client.Connect(ctx, clientSideTransport, nil)
|
|
if err != nil {
|
|
serverSession.Close()
|
|
return nil, err
|
|
}
|
|
|
|
return &innerConnection{
|
|
clientSession: clientSession,
|
|
cleanupFn: func() {
|
|
serverSession.Close()
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// capabilities performs an ephemeral in-memory connect to discover the
|
|
// server's advertised capabilities.
|
|
func (b *inMemoryBackend) capabilities(ctx context.Context) (*mcp.ServerCapabilities, error) {
|
|
st, ct := mcp.NewInMemoryTransports()
|
|
ss, err := b.server.Connect(ctx, st, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c := mcp.NewClient(&mcp.Implementation{Name: "cap-probe", Version: "1.0.0"}, nil)
|
|
cs, err := c.Connect(ctx, ct, nil)
|
|
if err != nil {
|
|
ss.Close()
|
|
return nil, err
|
|
}
|
|
|
|
var caps *mcp.ServerCapabilities
|
|
if ir := cs.InitializeResult(); ir != nil {
|
|
caps = ir.Capabilities
|
|
}
|
|
|
|
cs.Close()
|
|
ss.Close()
|
|
return caps, nil
|
|
}
|
|
|
|
// close is a no-op for in-memory backends.
|
|
func (b *inMemoryBackend) close() error {
|
|
return nil
|
|
}
|