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
36
go/sdk/examples/server/trading/README.md
Normal file
36
go/sdk/examples/server/trading/README.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Trading Platform
|
||||
|
||||
A variant-aware MCP server demonstrating API versioning and lifecycle management. Uses all three variant statuses (`stable`, `experimental`, `deprecated`) and `DeprecationInfo` to guide clients through API migrations.
|
||||
|
||||
**Pattern demonstrated:** Variant lifecycle, deprecation info, API versioning.
|
||||
|
||||
## Variants
|
||||
|
||||
| Variant | Status | Tools | Description |
|
||||
|---|---|---|---|
|
||||
| `v2-stable` | Stable | `place_order`, `get_quotes`, `get_portfolio`, `cancel_order` | Production API |
|
||||
| `v3-preview` | Experimental | `place_order_v3`, `stream_quotes`, `get_portfolio_v3` | Next-gen API with streaming |
|
||||
| `v1-legacy` | Deprecated | `trade`, `quote`, `balance` | Legacy API, removal date 2026-06-30 |
|
||||
| `analysis-only` | Stable | `get_quotes`, `get_portfolio`, `get_historical_data` | Read-only analytics subset |
|
||||
|
||||
## Deprecation Info
|
||||
|
||||
The `v1-legacy` variant includes `DeprecationInfo` with a replacement and scheduled removal date, demonstrating how servers communicate migration paths to clients:
|
||||
|
||||
```json
|
||||
{
|
||||
"deprecationInfo": {
|
||||
"message": "v1 API is deprecated. Migrate to v2-stable for improved order types, multi-symbol quotes, and portfolio tracking.",
|
||||
"replacement": "v2-stable",
|
||||
"removalDate": "2026-06-30"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
go run ./examples/server/trading
|
||||
```
|
||||
|
||||
The server listens on `http://localhost:8080`.
|
||||
130
go/sdk/examples/server/trading/main.go
Normal file
130
go/sdk/examples/server/trading/main.go
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
// Example: Trading Platform — API versioning and lifecycle management using
|
||||
// MCP server variants. Demonstrates DeprecationInfo, all three variant
|
||||
// statuses (stable, experimental, deprecated), and access control patterns
|
||||
// (read-only subset).
|
||||
//
|
||||
// Capability demonstrated: Variant lifecycle, DeprecationInfo, API migration.
|
||||
//
|
||||
// Variants:
|
||||
// - v2-stable: Production API with order management and portfolio tools
|
||||
// - v3-preview: Next-gen experimental API with streaming and enhanced orders
|
||||
// - v1-legacy: Deprecated legacy API scheduled for removal
|
||||
// - analysis-only: Read-only subset for analytics and monitoring
|
||||
//
|
||||
// Run:
|
||||
//
|
||||
// go run ./examples/server/trading
|
||||
//
|
||||
// Then connect any MCP client to http://localhost:8080.
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"github.com/modelcontextprotocol/experimental-ext-variants/go/sdk/variants"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// V2 stable: production API
|
||||
v2Server := mcp.NewServer(&mcp.Implementation{Name: "trading-platform", Version: "v2.0.0"}, nil)
|
||||
mcp.AddTool(v2Server, &mcp.Tool{
|
||||
Name: "place_order",
|
||||
Description: "Place a market or limit order for a given symbol",
|
||||
}, placeOrder)
|
||||
mcp.AddTool(v2Server, &mcp.Tool{
|
||||
Name: "get_quotes",
|
||||
Description: "Get real-time quotes for one or more symbols",
|
||||
}, getQuotes)
|
||||
mcp.AddTool(v2Server, &mcp.Tool{
|
||||
Name: "get_portfolio",
|
||||
Description: "Get portfolio positions and account balance",
|
||||
}, getPortfolio)
|
||||
mcp.AddTool(v2Server, &mcp.Tool{
|
||||
Name: "cancel_order",
|
||||
Description: "Cancel a pending order by order ID",
|
||||
}, cancelOrder)
|
||||
|
||||
// V3 preview: next-gen experimental API
|
||||
v3Server := mcp.NewServer(&mcp.Implementation{Name: "trading-platform", Version: "v3.0.0-preview"}, nil)
|
||||
mcp.AddTool(v3Server, &mcp.Tool{
|
||||
Name: "place_order_v3",
|
||||
Description: "Place an order with advanced options: stop/stop-limit types, time-in-force, and custom tags",
|
||||
}, placeOrderV3)
|
||||
mcp.AddTool(v3Server, &mcp.Tool{
|
||||
Name: "stream_quotes",
|
||||
Description: "Subscribe to real-time quote streaming for one or more symbols",
|
||||
}, streamQuotes)
|
||||
mcp.AddTool(v3Server, &mcp.Tool{
|
||||
Name: "get_portfolio_v3",
|
||||
Description: "Get enhanced portfolio with margin data and optional greeks",
|
||||
}, getPortfolioV3)
|
||||
|
||||
// V1 legacy: deprecated API scheduled for removal
|
||||
v1Server := mcp.NewServer(&mcp.Implementation{Name: "trading-platform", Version: "v1.0.0"}, nil)
|
||||
mcp.AddTool(v1Server, &mcp.Tool{
|
||||
Name: "trade",
|
||||
Description: "Execute a trade (deprecated — use place_order in v2-stable)",
|
||||
}, trade)
|
||||
mcp.AddTool(v1Server, &mcp.Tool{
|
||||
Name: "quote",
|
||||
Description: "Get a single stock quote (deprecated — use get_quotes in v2-stable)",
|
||||
}, quote)
|
||||
mcp.AddTool(v1Server, &mcp.Tool{
|
||||
Name: "balance",
|
||||
Description: "Get account balance (deprecated — use get_portfolio in v2-stable)",
|
||||
}, balance)
|
||||
|
||||
// Analysis-only: read-only subset for analytics
|
||||
analysisServer := mcp.NewServer(&mcp.Implementation{Name: "trading-platform", Version: "v2.0.0"}, nil)
|
||||
mcp.AddTool(analysisServer, &mcp.Tool{
|
||||
Name: "get_quotes",
|
||||
Description: "Get real-time quotes for one or more symbols",
|
||||
}, getQuotes)
|
||||
mcp.AddTool(analysisServer, &mcp.Tool{
|
||||
Name: "get_portfolio",
|
||||
Description: "Get portfolio positions and account balance (read-only view)",
|
||||
}, getPortfolio)
|
||||
mcp.AddTool(analysisServer, &mcp.Tool{
|
||||
Name: "get_historical_data",
|
||||
Description: "Get historical OHLCV price data for a symbol over a given period",
|
||||
}, getHistoricalData)
|
||||
|
||||
vs := variants.NewServer(&mcp.Implementation{Name: "trading-platform", Version: "v2.0.0"}).
|
||||
WithVariant(variants.ServerVariant{
|
||||
ID: "v2-stable",
|
||||
Description: "Production trading API (v2). Full order management with market/limit orders, real-time quotes, portfolio tracking, and order cancellation.",
|
||||
Hints: map[string]string{"com.example/apiGeneration": "v2", "contextSize": "standard"},
|
||||
Status: variants.Stable,
|
||||
}, v2Server, 0).
|
||||
WithVariant(variants.ServerVariant{
|
||||
ID: "v3-preview",
|
||||
Description: "Next-generation trading API (v3 preview). Adds stop/stop-limit orders, streaming quotes, margin data, and custom order tags. May change without notice.",
|
||||
Hints: map[string]string{"com.example/apiGeneration": "v3", "contextSize": "standard"},
|
||||
Status: variants.Experimental,
|
||||
}, v3Server, 1).
|
||||
WithVariant(variants.ServerVariant{
|
||||
ID: "v1-legacy",
|
||||
Description: "Legacy trading API (v1). Provides basic trade, quote, and balance operations. Scheduled for removal — migrate to v2-stable.",
|
||||
Hints: map[string]string{"com.example/apiGeneration": "v1", "contextSize": "compact"},
|
||||
Status: variants.Deprecated,
|
||||
DeprecationInfo: &variants.DeprecationInfo{
|
||||
Message: "v1 API is deprecated. Migrate to v2-stable for improved order types, multi-symbol quotes, and portfolio tracking.",
|
||||
Replacement: "v2-stable",
|
||||
RemovalDate: "2026-06-30",
|
||||
},
|
||||
}, v1Server, 2).
|
||||
WithVariant(variants.ServerVariant{
|
||||
ID: "analysis-only",
|
||||
Description: "Read-only analytics variant. Provides market data, portfolio viewing, and historical data without any order placement or modification capabilities.",
|
||||
Hints: map[string]string{"com.example/apiGeneration": "v2", "useCase": "planning", "contextSize": "standard"},
|
||||
Status: variants.Stable,
|
||||
}, analysisServer, 1)
|
||||
|
||||
handler := variants.NewStreamableHTTPHandler(vs, nil)
|
||||
|
||||
log.Println("Listening on :8080")
|
||||
log.Fatal(http.ListenAndServe(":8080", handler))
|
||||
}
|
||||
296
go/sdk/examples/server/trading/tools.go
Normal file
296
go/sdk/examples/server/trading/tools.go
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
// -- V2 tools -----------------------------------------------------------------
|
||||
|
||||
type PlaceOrderInput struct {
|
||||
Symbol string `json:"symbol" jsonschema:"ticker symbol (e.g. AAPL)"`
|
||||
Side string `json:"side" jsonschema:"order side: buy, sell"`
|
||||
Quantity int `json:"quantity" jsonschema:"number of shares"`
|
||||
Type string `json:"type,omitempty" jsonschema:"order type: market, limit (default: market)"`
|
||||
Price float64 `json:"price,omitempty" jsonschema:"limit price (required for limit orders)"`
|
||||
}
|
||||
|
||||
type PlaceOrderOutput struct {
|
||||
OrderID string `json:"orderId"`
|
||||
Symbol string `json:"symbol"`
|
||||
Side string `json:"side"`
|
||||
Status string `json:"status"`
|
||||
Price float64 `json:"filledPrice"`
|
||||
}
|
||||
|
||||
func placeOrder(_ context.Context, _ *mcp.CallToolRequest, in PlaceOrderInput) (*mcp.CallToolResult, PlaceOrderOutput, error) {
|
||||
price := in.Price
|
||||
if price == 0 {
|
||||
price = 185.50
|
||||
}
|
||||
return nil, PlaceOrderOutput{
|
||||
OrderID: "ORD-20250115-001",
|
||||
Symbol: in.Symbol,
|
||||
Side: in.Side,
|
||||
Status: "filled",
|
||||
Price: price,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type GetQuotesInput struct {
|
||||
Symbols []string `json:"symbols" jsonschema:"list of ticker symbols"`
|
||||
}
|
||||
|
||||
type Quote struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Bid float64 `json:"bid"`
|
||||
Ask float64 `json:"ask"`
|
||||
Last float64 `json:"last"`
|
||||
Volume int `json:"volume"`
|
||||
}
|
||||
|
||||
type GetQuotesOutput struct {
|
||||
Quotes []Quote `json:"quotes"`
|
||||
}
|
||||
|
||||
func getQuotes(_ context.Context, _ *mcp.CallToolRequest, in GetQuotesInput) (*mcp.CallToolResult, GetQuotesOutput, error) {
|
||||
quotes := make([]Quote, len(in.Symbols))
|
||||
for i, sym := range in.Symbols {
|
||||
quotes[i] = Quote{
|
||||
Symbol: sym,
|
||||
Bid: 185.40 + float64(i),
|
||||
Ask: 185.60 + float64(i),
|
||||
Last: 185.50 + float64(i),
|
||||
Volume: 1500000 + i*100000,
|
||||
}
|
||||
}
|
||||
return nil, GetQuotesOutput{Quotes: quotes}, nil
|
||||
}
|
||||
|
||||
type GetPortfolioInput struct {
|
||||
AccountID string `json:"accountId,omitempty" jsonschema:"account identifier (default: primary account)"`
|
||||
}
|
||||
|
||||
type Position struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Quantity int `json:"quantity"`
|
||||
AvgCost float64 `json:"avgCost"`
|
||||
Current float64 `json:"currentPrice"`
|
||||
PnL float64 `json:"unrealizedPnL"`
|
||||
}
|
||||
|
||||
type GetPortfolioOutput struct {
|
||||
AccountID string `json:"accountId"`
|
||||
Positions []Position `json:"positions"`
|
||||
CashBal float64 `json:"cashBalance"`
|
||||
TotalValue float64 `json:"totalValue"`
|
||||
}
|
||||
|
||||
func getPortfolio(_ context.Context, _ *mcp.CallToolRequest, in GetPortfolioInput) (*mcp.CallToolResult, GetPortfolioOutput, error) {
|
||||
return nil, GetPortfolioOutput{
|
||||
AccountID: "ACCT-001",
|
||||
Positions: []Position{
|
||||
{Symbol: "AAPL", Quantity: 100, AvgCost: 175.00, Current: 185.50, PnL: 1050.00},
|
||||
{Symbol: "GOOGL", Quantity: 50, AvgCost: 140.00, Current: 178.25, PnL: 1912.50},
|
||||
},
|
||||
CashBal: 25000.00,
|
||||
TotalValue: 52337.50,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type CancelOrderInput struct {
|
||||
OrderID string `json:"orderId" jsonschema:"order identifier to cancel"`
|
||||
}
|
||||
|
||||
type CancelOrderOutput struct {
|
||||
OrderID string `json:"orderId"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
func cancelOrder(_ context.Context, _ *mcp.CallToolRequest, in CancelOrderInput) (*mcp.CallToolResult, CancelOrderOutput, error) {
|
||||
return nil, CancelOrderOutput{
|
||||
OrderID: in.OrderID,
|
||||
Status: "cancelled",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// -- V3 tools -----------------------------------------------------------------
|
||||
|
||||
type PlaceOrderV3Input struct {
|
||||
Symbol string `json:"symbol" jsonschema:"ticker symbol"`
|
||||
Side string `json:"side" jsonschema:"order side: buy, sell, short"`
|
||||
Quantity int `json:"quantity" jsonschema:"number of shares"`
|
||||
Type string `json:"type,omitempty" jsonschema:"order type: market, limit, stop, stop_limit (default: market)"`
|
||||
Price float64 `json:"price,omitempty" jsonschema:"limit price"`
|
||||
StopPrice float64 `json:"stopPrice,omitempty" jsonschema:"stop trigger price"`
|
||||
TimeInForce string `json:"timeInForce,omitempty" jsonschema:"GTC, DAY, IOC, FOK (default: DAY)"`
|
||||
Tags map[string]string `json:"tags,omitempty" jsonschema:"custom order tags for tracking"`
|
||||
}
|
||||
|
||||
type PlaceOrderV3Output struct {
|
||||
OrderID string `json:"orderId"`
|
||||
Symbol string `json:"symbol"`
|
||||
Side string `json:"side"`
|
||||
Status string `json:"status"`
|
||||
Price float64 `json:"filledPrice"`
|
||||
TimeInForce string `json:"timeInForce"`
|
||||
}
|
||||
|
||||
func placeOrderV3(_ context.Context, _ *mcp.CallToolRequest, in PlaceOrderV3Input) (*mcp.CallToolResult, PlaceOrderV3Output, error) {
|
||||
price := in.Price
|
||||
if price == 0 {
|
||||
price = 185.50
|
||||
}
|
||||
tif := in.TimeInForce
|
||||
if tif == "" {
|
||||
tif = "DAY"
|
||||
}
|
||||
return nil, PlaceOrderV3Output{
|
||||
OrderID: "ORD-V3-20250115-001",
|
||||
Symbol: in.Symbol,
|
||||
Side: in.Side,
|
||||
Status: "filled",
|
||||
Price: price,
|
||||
TimeInForce: tif,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type StreamQuotesInput struct {
|
||||
Symbols []string `json:"symbols" jsonschema:"list of ticker symbols to stream"`
|
||||
Interval int `json:"interval,omitempty" jsonschema:"update interval in milliseconds (default: 1000)"`
|
||||
}
|
||||
|
||||
type StreamQuotesOutput struct {
|
||||
Snapshot []Quote `json:"snapshot"`
|
||||
StreamID string `json:"streamId"`
|
||||
}
|
||||
|
||||
func streamQuotes(_ context.Context, _ *mcp.CallToolRequest, in StreamQuotesInput) (*mcp.CallToolResult, StreamQuotesOutput, error) {
|
||||
quotes := make([]Quote, len(in.Symbols))
|
||||
for i, sym := range in.Symbols {
|
||||
quotes[i] = Quote{
|
||||
Symbol: sym,
|
||||
Bid: 185.40 + float64(i),
|
||||
Ask: 185.60 + float64(i),
|
||||
Last: 185.50 + float64(i),
|
||||
Volume: 1500000 + i*100000,
|
||||
}
|
||||
}
|
||||
return nil, StreamQuotesOutput{
|
||||
Snapshot: quotes,
|
||||
StreamID: "stream-001",
|
||||
}, nil
|
||||
}
|
||||
|
||||
type GetPortfolioV3Input struct {
|
||||
AccountID string `json:"accountId,omitempty" jsonschema:"account identifier (default: primary account)"`
|
||||
IncludeGreek bool `json:"includeGreeks,omitempty" jsonschema:"include options greeks in positions"`
|
||||
}
|
||||
|
||||
type GetPortfolioV3Output struct {
|
||||
AccountID string `json:"accountId"`
|
||||
Positions []Position `json:"positions"`
|
||||
CashBal float64 `json:"cashBalance"`
|
||||
TotalValue float64 `json:"totalValue"`
|
||||
MarginUsed float64 `json:"marginUsed"`
|
||||
}
|
||||
|
||||
func getPortfolioV3(_ context.Context, _ *mcp.CallToolRequest, in GetPortfolioV3Input) (*mcp.CallToolResult, GetPortfolioV3Output, error) {
|
||||
return nil, GetPortfolioV3Output{
|
||||
AccountID: "ACCT-001",
|
||||
Positions: []Position{
|
||||
{Symbol: "AAPL", Quantity: 100, AvgCost: 175.00, Current: 185.50, PnL: 1050.00},
|
||||
{Symbol: "GOOGL", Quantity: 50, AvgCost: 140.00, Current: 178.25, PnL: 1912.50},
|
||||
},
|
||||
CashBal: 25000.00,
|
||||
TotalValue: 52337.50,
|
||||
MarginUsed: 5000.00,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// -- V1 legacy tools ----------------------------------------------------------
|
||||
|
||||
type TradeInput struct {
|
||||
Ticker string `json:"ticker" jsonschema:"stock ticker"`
|
||||
Action string `json:"action" jsonschema:"buy or sell"`
|
||||
Shares int `json:"shares" jsonschema:"number of shares"`
|
||||
}
|
||||
|
||||
type TradeOutput struct {
|
||||
TradeID string `json:"tradeId"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
func trade(_ context.Context, _ *mcp.CallToolRequest, in TradeInput) (*mcp.CallToolResult, TradeOutput, error) {
|
||||
return nil, TradeOutput{
|
||||
TradeID: fmt.Sprintf("T-%s-%d", in.Ticker, in.Shares),
|
||||
Status: "executed",
|
||||
}, nil
|
||||
}
|
||||
|
||||
type QuoteInput struct {
|
||||
Ticker string `json:"ticker" jsonschema:"stock ticker"`
|
||||
}
|
||||
|
||||
type QuoteOutput struct {
|
||||
Ticker string `json:"ticker"`
|
||||
Price float64 `json:"price"`
|
||||
}
|
||||
|
||||
func quote(_ context.Context, _ *mcp.CallToolRequest, in QuoteInput) (*mcp.CallToolResult, QuoteOutput, error) {
|
||||
return nil, QuoteOutput{
|
||||
Ticker: in.Ticker,
|
||||
Price: 185.50,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type BalanceInput struct {
|
||||
Account string `json:"account,omitempty" jsonschema:"account name"`
|
||||
}
|
||||
|
||||
type BalanceOutput struct {
|
||||
Cash float64 `json:"cash"`
|
||||
Total float64 `json:"total"`
|
||||
}
|
||||
|
||||
func balance(_ context.Context, _ *mcp.CallToolRequest, in BalanceInput) (*mcp.CallToolResult, BalanceOutput, error) {
|
||||
return nil, BalanceOutput{
|
||||
Cash: 25000.00,
|
||||
Total: 52337.50,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// -- Analysis tools -----------------------------------------------------------
|
||||
|
||||
type GetHistoricalDataInput struct {
|
||||
Symbol string `json:"symbol" jsonschema:"ticker symbol"`
|
||||
Period string `json:"period,omitempty" jsonschema:"time period: 1d, 5d, 1mo, 3mo, 1y (default: 1mo)"`
|
||||
Interval string `json:"interval,omitempty" jsonschema:"data interval: 1m, 5m, 1h, 1d (default: 1d)"`
|
||||
}
|
||||
|
||||
type DataPoint struct {
|
||||
Date string `json:"date"`
|
||||
Open float64 `json:"open"`
|
||||
High float64 `json:"high"`
|
||||
Low float64 `json:"low"`
|
||||
Close float64 `json:"close"`
|
||||
Volume int `json:"volume"`
|
||||
}
|
||||
|
||||
type GetHistoricalDataOutput struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Data []DataPoint `json:"data"`
|
||||
}
|
||||
|
||||
func getHistoricalData(_ context.Context, _ *mcp.CallToolRequest, in GetHistoricalDataInput) (*mcp.CallToolResult, GetHistoricalDataOutput, error) {
|
||||
return nil, GetHistoricalDataOutput{
|
||||
Symbol: in.Symbol,
|
||||
Data: []DataPoint{
|
||||
{Date: "2025-01-13", Open: 183.00, High: 185.00, Low: 182.50, Close: 184.75, Volume: 1200000},
|
||||
{Date: "2025-01-14", Open: 184.75, High: 186.50, Low: 184.00, Close: 185.50, Volume: 1350000},
|
||||
{Date: "2025-01-15", Open: 185.50, High: 187.25, Low: 185.00, Close: 186.00, Volume: 1100000},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
Loading…
Reference in a new issue