Add example servers for SEP-2053 variant patterns (#4)
Add four new example servers demonstrating different SEP-2053 variant patterns: - model-optimized (same tools, different descriptions per LLM family) - trading (API versioning with deprecation lifecycle) - research (context budget management) - Refactor the existing variants-http example into a GitHub-style server with custom ranking. Signed-off-by: Kurt Degiorgio <kdegiorgio@bloomberg.net>
This commit is contained in:
parent
c01fd188d2
commit
e1c17e047f
3 changed files with 1218 additions and 147 deletions
118
go/sdk/examples/server/model-optimized/README.md
Normal file
118
go/sdk/examples/server/model-optimized/README.md
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
# Model-Optimized Weather Service
|
||||
|
||||
A variant-aware MCP server that exposes the same tools (`get_weather`, `get_forecast`) with different descriptions optimized for different LLM families. This is the primary use case described in SEP-2053.
|
||||
|
||||
**Pattern demonstrated:** Same tools, different descriptions per variant.
|
||||
|
||||
## Variants
|
||||
|
||||
| Variant | Description Style | Target |
|
||||
|---|---|---|
|
||||
| `claude-optimized` | Verbose, structured with explicit usage guidance | Anthropic Claude models |
|
||||
| `gpt-optimized` | Concise, function-style with JSON Schema emphasis | OpenAI GPT models |
|
||||
| `compact` | Minimal, bare-minimum descriptions | Context-constrained environments |
|
||||
|
||||
All three variants expose the same two tools with the same behavior — only the tool descriptions differ.
|
||||
|
||||
## Custom Ranking
|
||||
|
||||
Clients send a `"modelFamily"` hint (e.g., `"anthropic"`, `"openai"`). The ranking function matches the hint to the variant's `modelFamily`, falling back to priority order.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
go run ./examples/server/model-optimized
|
||||
```
|
||||
The server listens on `http://localhost:8080`.
|
||||
|
||||
Connect any MCP client to `http://localhost:8080`.
|
||||
|
||||
## Try It
|
||||
|
||||
### 1. Initialize without hints (default ranking)
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://localhost:8080/mcp \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"jsonrpc": "2.0", "id": 1,
|
||||
"method": "initialize",
|
||||
"params": {
|
||||
"protocolVersion": "2025-03-26",
|
||||
"clientInfo": {"name": "test", "version": "1.0"},
|
||||
"capabilities": {}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
The first variant in the response is the **default** — clients that don't specify a variant in subsequent requests will use it. Without hints, `claude-optimized` is the default:
|
||||
|
||||
```json
|
||||
{
|
||||
"availableVariants": [
|
||||
{
|
||||
"id": "claude-optimized",
|
||||
"hints": { "contextSize": "verbose", "modelFamily": "anthropic" },
|
||||
"status": "stable"
|
||||
},
|
||||
{
|
||||
"id": "gpt-optimized",
|
||||
"hints": { "contextSize": "standard", "modelFamily": "openai" },
|
||||
"status": "stable"
|
||||
},
|
||||
{
|
||||
"id": "compact",
|
||||
"hints": { "contextSize": "compact", "modelFamily": "any" },
|
||||
"status": "stable"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Initialize with an OpenAI hint (ranking changes)
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://localhost:8080/mcp \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"jsonrpc": "2.0", "id": 1,
|
||||
"method": "initialize",
|
||||
"params": {
|
||||
"protocolVersion": "2025-03-26",
|
||||
"clientInfo": {"name": "test", "version": "1.0"},
|
||||
"capabilities": {
|
||||
"experimental": {
|
||||
"io.modelcontextprotocol/server-variants": {
|
||||
"variantHints": {
|
||||
"hints": {"modelFamily": "openai"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
Now `gpt-optimized` is ranked first and becomes the default:
|
||||
|
||||
```json
|
||||
{
|
||||
"availableVariants": [
|
||||
{
|
||||
"id": "gpt-optimized",
|
||||
"hints": { "contextSize": "standard", "modelFamily": "openai" },
|
||||
"status": "stable"
|
||||
},
|
||||
{
|
||||
"id": "claude-optimized",
|
||||
"hints": { "contextSize": "verbose", "modelFamily": "anthropic" },
|
||||
"status": "stable"
|
||||
},
|
||||
{
|
||||
"id": "compact",
|
||||
"hints": { "contextSize": "compact", "modelFamily": "any" },
|
||||
"status": "stable"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
116
go/sdk/examples/server/model-optimized/main.go
Normal file
116
go/sdk/examples/server/model-optimized/main.go
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
// Example: Model-Optimized Weather Service — the same tools with different
|
||||
// descriptions and schemas optimized for different LLM families. This
|
||||
// demonstrates the SEP-2053 primary use case where tool descriptions are
|
||||
// tailored per model family for optimal tool-calling performance.
|
||||
//
|
||||
// Capability demonstrated: Same tools, different descriptions per variant.
|
||||
//
|
||||
// Variants:
|
||||
// - claude-optimized: Detailed, structured descriptions with explicit guidance
|
||||
// - gpt-optimized: Concise function-style descriptions with JSON Schema emphasis
|
||||
// - compact: Minimal descriptions for context-constrained environments
|
||||
//
|
||||
// Run:
|
||||
//
|
||||
// go run ./examples/server/model-optimized
|
||||
//
|
||||
// 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() {
|
||||
// Claude-optimized variant: verbose, structured descriptions with
|
||||
// explicit guidance on when and how to use each tool.
|
||||
claudeServer := mcp.NewServer(&mcp.Implementation{Name: "weather-service", Version: "v1.0.0"}, nil)
|
||||
mcp.AddTool(claudeServer, &mcp.Tool{
|
||||
Name: "get_weather",
|
||||
Description: "Get the current weather conditions for a specific location. " +
|
||||
"Use this tool when the user asks about current weather, temperature, or conditions. " +
|
||||
"Accepts city names (e.g., 'San Francisco, CA'), country names, or lat/long coordinates. " +
|
||||
"Returns temperature, humidity, wind speed, and a human-readable condition description. " +
|
||||
"Prefer this over get_forecast when the user only needs current conditions.",
|
||||
}, getWeather)
|
||||
mcp.AddTool(claudeServer, &mcp.Tool{
|
||||
Name: "get_forecast",
|
||||
Description: "Get a multi-day weather forecast for a specific location. " +
|
||||
"Use this tool when the user asks about future weather, upcoming conditions, or wants to plan ahead. " +
|
||||
"Returns daily high/low temperatures and conditions for 1-7 days. " +
|
||||
"Defaults to 3 days if not specified. Use get_weather instead if only current conditions are needed.",
|
||||
}, getForecast)
|
||||
|
||||
// GPT-optimized variant: concise function-style descriptions with
|
||||
// JSON Schema emphasis for OpenAI-family models.
|
||||
gptServer := mcp.NewServer(&mcp.Implementation{Name: "weather-service", Version: "v1.0.0"}, nil)
|
||||
mcp.AddTool(gptServer, &mcp.Tool{
|
||||
Name: "get_weather",
|
||||
Description: "Returns current weather data (temperature, humidity, wind, conditions) for a given location.",
|
||||
}, getWeather)
|
||||
mcp.AddTool(gptServer, &mcp.Tool{
|
||||
Name: "get_forecast",
|
||||
Description: "Returns multi-day forecast (high/low temps, conditions) for a location. Days: 1-7, default 3.",
|
||||
}, getForecast)
|
||||
|
||||
// Compact variant: minimal descriptions for context-constrained
|
||||
// environments where token budget is limited.
|
||||
compactServer := mcp.NewServer(&mcp.Implementation{Name: "weather-service", Version: "v1.0.0"}, nil)
|
||||
mcp.AddTool(compactServer, &mcp.Tool{
|
||||
Name: "get_weather",
|
||||
Description: "Current weather for a location.",
|
||||
}, getWeather)
|
||||
mcp.AddTool(compactServer, &mcp.Tool{
|
||||
Name: "get_forecast",
|
||||
Description: "Multi-day forecast for a location.",
|
||||
}, getForecast)
|
||||
|
||||
vs := variants.NewServer(&mcp.Implementation{Name: "weather-service", Version: "v1.0.0"}).
|
||||
WithVariant(variants.ServerVariant{
|
||||
ID: "claude-optimized",
|
||||
Description: "Detailed, structured tool descriptions with explicit usage guidance. Optimized for Anthropic Claude models that benefit from rich context and clear instructions.",
|
||||
Hints: map[string]string{"modelFamily": "anthropic", "contextSize": "verbose"},
|
||||
Status: variants.Stable,
|
||||
}, claudeServer, 0).
|
||||
WithVariant(variants.ServerVariant{
|
||||
ID: "gpt-optimized",
|
||||
Description: "Concise function-style descriptions with JSON Schema emphasis. Optimized for OpenAI GPT models that work well with terse, specification-like tool definitions.",
|
||||
Hints: map[string]string{"modelFamily": "openai", "contextSize": "standard"},
|
||||
Status: variants.Stable,
|
||||
}, gptServer, 1).
|
||||
WithVariant(variants.ServerVariant{
|
||||
ID: "compact",
|
||||
Description: "Minimal tool descriptions for context-constrained environments. Use when token budget is severely limited or tools are self-explanatory from their schemas.",
|
||||
Hints: map[string]string{"modelFamily": "any", "contextSize": "compact"},
|
||||
Status: variants.Stable,
|
||||
}, compactServer, 2).
|
||||
// Custom ranking: match by modelFamily hint, fall back to priority.
|
||||
WithRanking(func(_ context.Context, hints variants.VariantHints, vs []variants.ServerVariant) []variants.ServerVariant {
|
||||
requested, _ := variants.HintValue[string](hints, "modelFamily")
|
||||
slices.SortStableFunc(vs, func(a, b variants.ServerVariant) int {
|
||||
aMatch := strings.EqualFold(a.Hints["modelFamily"], requested)
|
||||
bMatch := strings.EqualFold(b.Hints["modelFamily"], 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))
|
||||
}
|
||||
91
go/sdk/examples/server/model-optimized/tools.go
Normal file
91
go/sdk/examples/server/model-optimized/tools.go
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
type GetWeatherInput struct {
|
||||
Location string `json:"location" jsonschema:"location name or coordinates (e.g. 'San Francisco, CA' or '37.7749,-122.4194')"`
|
||||
Units string `json:"units,omitempty" jsonschema:"temperature units: celsius, fahrenheit (default: celsius)"`
|
||||
}
|
||||
|
||||
type GetWeatherOutput struct {
|
||||
Location string `json:"location"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
Units string `json:"units"`
|
||||
Condition string `json:"condition"`
|
||||
Humidity int `json:"humidity"`
|
||||
WindSpeed float64 `json:"windSpeed"`
|
||||
}
|
||||
|
||||
type GetForecastInput struct {
|
||||
Location string `json:"location" jsonschema:"location name or coordinates"`
|
||||
Days int `json:"days,omitempty" jsonschema:"number of days to forecast (1-7, default: 3)"`
|
||||
Units string `json:"units,omitempty" jsonschema:"temperature units: celsius, fahrenheit (default: celsius)"`
|
||||
}
|
||||
|
||||
type ForecastDay struct {
|
||||
Date string `json:"date"`
|
||||
High float64 `json:"high"`
|
||||
Low float64 `json:"low"`
|
||||
Condition string `json:"condition"`
|
||||
}
|
||||
|
||||
type GetForecastOutput struct {
|
||||
Location string `json:"location"`
|
||||
Units string `json:"units"`
|
||||
Forecast []ForecastDay `json:"forecast"`
|
||||
}
|
||||
|
||||
func getWeather(_ context.Context, _ *mcp.CallToolRequest, in GetWeatherInput) (*mcp.CallToolResult, GetWeatherOutput, error) {
|
||||
units := in.Units
|
||||
if units == "" {
|
||||
units = "celsius"
|
||||
}
|
||||
temp := 22.5
|
||||
if units == "fahrenheit" {
|
||||
temp = 72.5
|
||||
}
|
||||
return nil, GetWeatherOutput{
|
||||
Location: in.Location,
|
||||
Temperature: temp,
|
||||
Units: units,
|
||||
Condition: "partly cloudy",
|
||||
Humidity: 65,
|
||||
WindSpeed: 12.3,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getForecast(_ context.Context, _ *mcp.CallToolRequest, in GetForecastInput) (*mcp.CallToolResult, GetForecastOutput, error) {
|
||||
units := in.Units
|
||||
if units == "" {
|
||||
units = "celsius"
|
||||
}
|
||||
days := in.Days
|
||||
if days == 0 {
|
||||
days = 3
|
||||
}
|
||||
forecast := make([]ForecastDay, days)
|
||||
conditions := []string{"sunny", "partly cloudy", "cloudy", "light rain", "clear", "overcast", "scattered showers"}
|
||||
for i := range forecast {
|
||||
high := 24.0 + float64(i)
|
||||
low := 14.0 + float64(i)
|
||||
if units == "fahrenheit" {
|
||||
high = 75.0 + float64(i)*2
|
||||
low = 57.0 + float64(i)*2
|
||||
}
|
||||
forecast[i] = ForecastDay{
|
||||
Date: "2025-01-0" + string(rune('1'+i)),
|
||||
High: high,
|
||||
Low: low,
|
||||
Condition: conditions[i%len(conditions)],
|
||||
}
|
||||
}
|
||||
return nil, GetForecastOutput{
|
||||
Location: in.Location,
|
||||
Units: units,
|
||||
Forecast: forecast,
|
||||
}, nil
|
||||
}
|
||||
Loading…
Reference in a new issue