experimental-ext-variants/go/sdk/examples/server/research/main.go
Steven Chen e5e8f117e8
variants: redirect inner server sending methods to fix dropping notifications in stateless mode (#7)
Registers a sending middleware on each inner server that intercepts all
outgoing messages and routes them through the front session using a
session-swapped request wrapper. Replaces the previous proxy client
handler approach which dropped log messages in stateless mode.

## Motivation and Context
Tool handlers in variant servers use req.Session (the inner session) to
send notifications. Previously, log messages were silently dropped in
stateless mode because the inner session is not aware of the front
session.
This change injects the sendingMethodHandler from the frontSession into
the context and adds a sending middleware to redirect all sending
requests to the frontSession handler directly.
A few considerations in this implementation:
* Having frontSession in the context allows notifications to be sent to
the correct frontSession, when the same backend session could be used by
multiple front sessions.
* This solution will work for synchronous notifications that happen
within the same context (progress, log, sampling, elicitation) but not
working for asynchronous notifications (tool list changed, resource
updated etc), because asynchronous notifications are sent with a new
context.
* All the operations before the sending method will still happen in the
backend. Most notable impact is in stateless mode, requests from one
client could change the log level for other clients. For this reason, I
default the logging level to debug for now.
* This implementation relies on the MCP Go SDK sendingMethodHandler
taking a generic `Request` not one that tied to specific function. Based
on the current SDK implementation this is unlikely to change.
* The same generic method forwarding mechanism could work for receiving
methods too, but with some caveat. That can be addressed in separate PR.

## How Has This Been Tested?
TestNotificationDelivery — verifies progress and log messages arrive at
the front clients with multiple clients making concurrent request.

New tests added for context preservation that was added in
https://github.com/modelcontextprotocol/experimental-ext-variants/pull/6.
Context preservation is required to make the notification forwarding
work anyway. Since we use the context to carry frontSession handler to
the backend session.

All existing tests pass

## Breaking Changes
None. Tool handlers continue to use req.Session normally.

## Types of changes
Bug fix (non-breaking change which fixes an issue)
2026-03-23 15:39:57 +00:00

150 lines
7.1 KiB
Go

// Example: Research Assistant — context budget management with the same tools
// at different verbosity levels. Demonstrates how variants can optimize tool
// descriptions for different context window sizes and use cases.
//
// Each tool sends progress notifications and log messages as it works,
// simulating a deep-research workflow where the client can observe each
// step (e.g. "Searching arXiv", "Resolving references").
//
// Capability demonstrated: Context budget management, description verbosity,
// notification streaming.
//
// Variants:
// - deep-research: Verbose multi-paragraph descriptions with usage examples
// - quick-lookup: Concise 1-sentence descriptions for fast Q&A
// - synthesis: Balanced descriptions for report generation
//
// Run:
//
// go run ./examples/server/research
//
// Then connect any MCP client to http://localhost:8080.
package main
import (
"context"
"log"
"net/http"
"slices"
"strings"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/modelcontextprotocol/experimental-ext-variants/go/sdk/variants"
)
func main() {
// Deep-research variant: verbose descriptions with usage examples
// for thorough research workflows with large context windows.
deepServer := mcp.NewServer(&mcp.Implementation{Name: "research-assistant", Version: "v1.0.0"}, &mcp.ServerOptions{
Capabilities: &mcp.ServerCapabilities{Logging: &mcp.LoggingCapabilities{}},
})
mcp.AddTool(deepServer, &mcp.Tool{
Name: "search_papers",
Description: "Search academic papers across multiple databases including arXiv, Semantic Scholar, and PubMed. " +
"Use this tool to find relevant literature for a research topic. Supports filtering by year and field. " +
"Returns paper metadata including titles, authors, abstracts, and citation counts. " +
"For best results, use specific technical terms rather than broad queries. " +
"Example queries: 'transformer attention mechanisms in computer vision', " +
"'reinforcement learning from human feedback alignment'. " +
"Follow up with get_paper to retrieve full details for promising results.",
}, searchPapers)
mcp.AddTool(deepServer, &mcp.Tool{
Name: "get_paper",
Description: "Retrieve full details for a specific academic paper by its identifier. " +
"Accepts arXiv IDs (e.g., arxiv:2301.01234), DOIs (e.g., doi:10.1234/5678), or Semantic Scholar IDs. " +
"Returns complete metadata including abstract, references, keywords, and publication venue. " +
"Use this after search_papers to get deeper context on papers of interest. " +
"The references list can be used to find related work by calling get_paper recursively. " +
"Combine with summarize to generate concise summaries of lengthy papers.",
}, getPaper)
mcp.AddTool(deepServer, &mcp.Tool{
Name: "summarize",
Description: "Generate a summary of the provided text using various styles and lengths. " +
"Styles: 'abstract' produces a formal academic abstract, 'bullet-points' extracts key findings " +
"as a structured list, 'executive' creates a high-level overview for non-technical audiences. " +
"Lengths: 'short' (1-2 sentences), 'medium' (1 paragraph), 'long' (2-3 paragraphs). " +
"Best used with paper abstracts or full text retrieved via get_paper. " +
"For multi-paper synthesis, concatenate abstracts and use the 'executive' style.",
}, summarize)
// Quick-lookup variant: concise descriptions for fast Q&A sessions
// where context budget is limited.
quickServer := mcp.NewServer(&mcp.Implementation{Name: "research-assistant", Version: "v1.0.0"}, &mcp.ServerOptions{
Capabilities: &mcp.ServerCapabilities{Logging: &mcp.LoggingCapabilities{}},
})
mcp.AddTool(quickServer, &mcp.Tool{
Name: "search_papers",
Description: "Search academic papers by query, with optional year and field filters.",
}, searchPapers)
mcp.AddTool(quickServer, &mcp.Tool{
Name: "get_paper",
Description: "Get full details for a paper by its ID (arXiv, DOI, etc.).",
}, getPaper)
mcp.AddTool(quickServer, &mcp.Tool{
Name: "summarize",
Description: "Summarize text in abstract, bullet-point, or executive style.",
}, summarize)
// Synthesis variant: balanced descriptions for report generation
// workflows that need moderate detail.
synthesisServer := mcp.NewServer(&mcp.Implementation{Name: "research-assistant", Version: "v1.0.0"}, &mcp.ServerOptions{
Capabilities: &mcp.ServerCapabilities{Logging: &mcp.LoggingCapabilities{}},
})
mcp.AddTool(synthesisServer, &mcp.Tool{
Name: "search_papers",
Description: "Search academic papers across arXiv, Semantic Scholar, and PubMed. " +
"Returns titles, authors, abstracts, and citation counts. Filter by year and field.",
}, searchPapers)
mcp.AddTool(synthesisServer, &mcp.Tool{
Name: "get_paper",
Description: "Retrieve full paper details including abstract, references, keywords, and venue. " +
"Accepts arXiv IDs, DOIs, or Semantic Scholar IDs.",
}, getPaper)
mcp.AddTool(synthesisServer, &mcp.Tool{
Name: "summarize",
Description: "Generate a summary in abstract, bullet-point, or executive style. " +
"Configurable length: short (1-2 sentences), medium (paragraph), or long (2-3 paragraphs).",
}, summarize)
vs := variants.NewServer(&mcp.Implementation{Name: "research-assistant", Version: "v1.0.0"}).
WithVariant(variants.ServerVariant{
ID: "deep-research",
Description: "Verbose tool descriptions with usage examples and guidance for thorough research workflows. Best for agents with large context windows performing literature reviews or deep analysis.",
Hints: map[string]string{"contextSize": "verbose", "useCase": "research"},
Status: variants.Stable,
}, deepServer, 0).
WithVariant(variants.ServerVariant{
ID: "quick-lookup",
Description: "Concise 1-sentence tool descriptions for fast question-answering. Minimal context usage for agents with limited token budgets or simple lookup tasks.",
Hints: map[string]string{"contextSize": "compact", "useCase": "qa"},
Status: variants.Stable,
}, quickServer, 1).
WithVariant(variants.ServerVariant{
ID: "synthesis",
Description: "Balanced tool descriptions for report generation and multi-paper synthesis. Moderate detail level suitable for structured writing workflows.",
Hints: map[string]string{"contextSize": "standard", "useCase": "synthesis"},
Status: variants.Experimental,
}, synthesisServer, 2).
// Custom ranking: match by contextSize hint, fall back to priority.
WithRanking(func(_ context.Context, hints variants.VariantHints, vs []variants.ServerVariant) []variants.ServerVariant {
requested, _ := variants.HintValue[string](hints, "contextSize")
slices.SortStableFunc(vs, func(a, b variants.ServerVariant) int {
aMatch := strings.EqualFold(a.Hints["contextSize"], requested)
bMatch := strings.EqualFold(b.Hints["contextSize"], requested)
if aMatch != bMatch {
if aMatch {
return -1
}
return 1
}
return a.Priority() - b.Priority()
})
return vs
})
handler := variants.NewStreamableHTTPHandler(vs, nil)
log.Println("Listening on :8080")
log.Fatal(http.ListenAndServe(":8080", handler))
}