diff --git a/README.md b/README.md index 0afa166..c944565 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,34 @@ This repository provides a multi-language reference implementation of the varian | TypeScript | `typescript/sdk/` | `@ext-modelcontextprotocol/variants` | Planned | +## Examples (Go) + +The Go implementation includes runnable example servers under [`go/sdk/examples/server/`](go/sdk/examples/server/): + +### [`model-optimized/`](go/sdk/examples/server/model-optimized/) + +Same tools, different descriptions per LLM family. + +https://github.com/user-attachments/assets/5bb60cd2-291f-4940-abfe-f53852d35470 + +### [`github/`](go/sdk/examples/server/github/) + +Different tool sets per variant with custom ranking. + +https://github.com/user-attachments/assets/d770ac55-e988-4d89-bdf8-bcb3cf1e2e53 + +### [`research/`](go/sdk/examples/server/research/) + +Context budget management via description verbosity. + +### [`trading/`](go/sdk/examples/server/trading/) + +API versioning, lifecycle statuses, and deprecation info. + +### [`variants-stdio/`](go/sdk/examples/server/variants-stdio/) + +Minimal single-variant setup. + ## CI/CD This monorepo uses **path-based CI workflows** to efficiently test only what changes: diff --git a/go/sdk/README.md b/go/sdk/README.md index ab7a783..9cbaaea 100644 --- a/go/sdk/README.md +++ b/go/sdk/README.md @@ -138,10 +138,7 @@ In **stateful mode** (default, stdio and HTTP), per-session inner connections ar ## Examples -See [`examples/server/`](examples/server/) for runnable examples: - -- [`variants/`](examples/server/variants/) — stdio transport (single client) -- [`variants-http/`](examples/server/variants-http/) — HTTP transport (multiple concurrent clients) +See [`examples/server/`](examples/server/) for runnable examples. ## API diff --git a/go/sdk/examples/server/github/README.md b/go/sdk/examples/server/github/README.md new file mode 100644 index 0000000..7be5eee --- /dev/null +++ b/go/sdk/examples/server/github/README.md @@ -0,0 +1,30 @@ +# GitHub Developer Platform + +A variant-aware MCP server that exposes different tool sets for different agent types, mirroring the GitHub MCP Server pattern described in SEP-2053's Prior Art section. + +**Pattern demonstrated:** Different tool sets per variant with custom ranking. + +## Variants + +| Variant | Tools | Description | +|---|---|---| +| `code-review` | `list_pull_requests`, `get_diff`, `add_review_comment` | PR operations and code review | +| `project-management` | `list_issues`, `create_issue`, `add_label` | Issue tracking and labels | +| `security-readonly` | `list_security_alerts`, `get_advisory` | Security scanning (read-only) | +| `ci-automation` | `list_workflow_runs`, `trigger_workflow` | CI/CD workflow management | + +## Custom Ranking + +Clients send a `"domain"` hint during initialization. The ranking function boosts variants whose `domain` hint matches the client's requested domain, falling back to priority order. + +## Run + +```bash +go run ./examples/server/github +``` + +Connect any MCP client to `http://localhost:8080`. + +## Demo + +See [mcp-inspector-variants-demo.mp4](mcp-inspector-variants-demo.mp4) for a walkthrough using MCP Inspector. diff --git a/go/sdk/examples/server/variants-http/main.go b/go/sdk/examples/server/github/main.go similarity index 75% rename from go/sdk/examples/server/variants-http/main.go rename to go/sdk/examples/server/github/main.go index 6e7a80e..96e7d6f 100644 --- a/go/sdk/examples/server/variants-http/main.go +++ b/go/sdk/examples/server/github/main.go @@ -1,6 +1,10 @@ -// Example: A developer productivity platform (à la GitHub) that exposes -// different tool sets for different agent types using MCP server variants, -// served over HTTP for multiple concurrent clients. +// Example: GitHub MCP Server — a developer productivity platform that exposes +// different tool sets for different agent types using MCP server variants. +// This mirrors the GitHub MCP Server pattern described in SEP-2053's Prior Art +// section, where a single server exposes code review, project management, +// security, and CI/CD tools as separate variants with custom ranking. +// +// Capability demonstrated: Different tool sets per variant, custom ranking. // // Variants: // - code-review: PR operations, diffs, and review comments @@ -10,18 +14,20 @@ // // Run: // -// go run . +// go run ./examples/server/github // // 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/examples/server/exampletools" "github.com/modelcontextprotocol/experimental-ext-variants/go/sdk/variants" ) @@ -31,52 +37,52 @@ func main() { mcp.AddTool(codeReviewServer, &mcp.Tool{ Name: "list_pull_requests", Description: "List open pull requests, optionally filtered by author", - }, exampletools.ListPullRequests) + }, listPullRequests) mcp.AddTool(codeReviewServer, &mcp.Tool{ Name: "get_diff", Description: "Get the diff for a pull request, including changed files and line counts", - }, exampletools.GetDiff) + }, getDiff) mcp.AddTool(codeReviewServer, &mcp.Tool{ Name: "add_review_comment", Description: "Post a review comment on a specific line of a pull request", - }, exampletools.AddReviewComment) + }, addReviewComment) // Project management variant: issue-focused tools for PM agents pmServer := mcp.NewServer(&mcp.Implementation{Name: "devplatform", Version: "v1.0.0"}, nil) mcp.AddTool(pmServer, &mcp.Tool{ Name: "list_issues", Description: "List issues, optionally filtered by state and labels", - }, exampletools.ListIssues) + }, listIssues) mcp.AddTool(pmServer, &mcp.Tool{ Name: "create_issue", Description: "Create a new issue with title, body, and optional labels", - }, exampletools.CreateIssue) + }, createIssue) mcp.AddTool(pmServer, &mcp.Tool{ Name: "add_label", Description: "Add labels to an existing issue", - }, exampletools.AddLabel) + }, addLabel) // Security variant: read-only security scanning tools securityServer := mcp.NewServer(&mcp.Implementation{Name: "devplatform", Version: "v1.0.0"}, nil) mcp.AddTool(securityServer, &mcp.Tool{ Name: "list_security_alerts", Description: "List code scanning alerts for a repository", - }, exampletools.ListSecurityAlerts) + }, listSecurityAlerts) mcp.AddTool(securityServer, &mcp.Tool{ Name: "get_advisory", Description: "Get details of a security advisory", - }, exampletools.GetAdvisory) + }, getAdvisory) // CI/CD variant: workflow management tools for automation agents ciServer := mcp.NewServer(&mcp.Implementation{Name: "devplatform", Version: "v1.0.0"}, nil) mcp.AddTool(ciServer, &mcp.Tool{ Name: "list_workflow_runs", Description: "List recent workflow runs for a repository", - }, exampletools.ListWorkflowRuns) + }, listWorkflowRuns) mcp.AddTool(ciServer, &mcp.Tool{ Name: "trigger_workflow", Description: "Trigger a workflow dispatch event", - }, exampletools.TriggerWorkflow) + }, triggerWorkflow) vs := variants.NewServer(&mcp.Implementation{Name: "devplatform", Version: "v1.0.0"}). WithVariant(variants.ServerVariant{ @@ -102,7 +108,23 @@ func main() { Description: "CI/CD workflow management. Trigger runs, monitor jobs, manage deployments designed for automation agents.", Hints: map[string]string{"domain": "ci-cd", "accessLevel": "automation"}, Status: variants.Stable, - }, ciServer, 3) + }, ciServer, 3). + // Custom ranking: boost variants whose "domain" hint matches the client's. + WithRanking(func(_ context.Context, hints variants.VariantHints, vs []variants.ServerVariant) []variants.ServerVariant { + requested, _ := variants.HintValue[string](hints, "domain") + slices.SortStableFunc(vs, func(a, b variants.ServerVariant) int { + aMatch := strings.Contains(strings.ToLower(a.Hints["domain"]), strings.ToLower(requested)) + bMatch := strings.Contains(strings.ToLower(b.Hints["domain"]), strings.ToLower(requested)) + if aMatch != bMatch { + if aMatch { + return -1 + } + return 1 + } + return a.Priority() - b.Priority() + }) + return vs + }) handler := variants.NewStreamableHTTPHandler(vs, nil) diff --git a/go/sdk/examples/server/exampletools/tools.go b/go/sdk/examples/server/github/tools.go similarity index 88% rename from go/sdk/examples/server/exampletools/tools.go rename to go/sdk/examples/server/github/tools.go index 85561ec..67b0c42 100644 --- a/go/sdk/examples/server/exampletools/tools.go +++ b/go/sdk/examples/server/github/tools.go @@ -1,12 +1,4 @@ -// Package exampletools defines the shared tool types and handlers used by the -// variant-aware MCP server examples. The tools simulate a developer -// productivity platform (à la GitHub/GitLab) with four variants: -// -// - code-review: PR operations, diffs, and review comments -// - project-management: issue tracking, labels, and assignments -// - security-readonly: security scanning alerts and advisories (read-only) -// - ci-automation: CI/CD workflow management and dispatch -package exampletools +package main import ( "context" @@ -34,7 +26,7 @@ type ListPullRequestsOutput struct { PullRequests []PullRequest `json:"pullRequests"` } -func ListPullRequests(_ context.Context, _ *mcp.CallToolRequest, in ListPullRequestsInput) (*mcp.CallToolResult, ListPullRequestsOutput, error) { +func listPullRequests(_ context.Context, _ *mcp.CallToolRequest, in ListPullRequestsInput) (*mcp.CallToolResult, ListPullRequestsOutput, error) { return nil, ListPullRequestsOutput{ PullRequests: []PullRequest{ {Number: 42, Title: "Add retry logic to API client", Author: "alice", State: "open"}, @@ -56,7 +48,7 @@ type GetDiffOutput struct { Deletions int `json:"deletions"` } -func GetDiff(_ context.Context, _ *mcp.CallToolRequest, in GetDiffInput) (*mcp.CallToolResult, GetDiffOutput, error) { +func getDiff(_ context.Context, _ *mcp.CallToolRequest, in GetDiffInput) (*mcp.CallToolResult, GetDiffOutput, error) { return nil, GetDiffOutput{ Diff: "--- a/client.go\n+++ b/client.go\n@@ -45,6 +45,12 @@\n+ for attempt := 0; attempt < maxRetries; attempt++ {\n+ resp, err := c.do(req)\n+ if err == nil { return resp, nil }\n+ time.Sleep(backoff(attempt))\n+ }", Files: []string{"client.go", "client_test.go"}, @@ -78,7 +70,7 @@ type AddReviewCommentOutput struct { URL string `json:"url"` } -func AddReviewComment(_ context.Context, _ *mcp.CallToolRequest, in AddReviewCommentInput) (*mcp.CallToolResult, AddReviewCommentOutput, error) { +func addReviewComment(_ context.Context, _ *mcp.CallToolRequest, in AddReviewCommentInput) (*mcp.CallToolResult, AddReviewCommentOutput, error) { return nil, AddReviewCommentOutput{ CommentID: 1001, URL: fmt.Sprintf("https://github.com/%s/pull/%d#discussion_r1001", in.Repo, in.Number), @@ -105,7 +97,7 @@ type ListIssuesOutput struct { Issues []Issue `json:"issues"` } -func ListIssues(_ context.Context, _ *mcp.CallToolRequest, in ListIssuesInput) (*mcp.CallToolResult, ListIssuesOutput, error) { +func listIssues(_ context.Context, _ *mcp.CallToolRequest, in ListIssuesInput) (*mcp.CallToolResult, ListIssuesOutput, error) { return nil, ListIssuesOutput{ Issues: []Issue{ {Number: 101, Title: "API rate limiting returns wrong status code", State: "open", Labels: []string{"bug", "api"}, Assignee: "alice"}, @@ -127,7 +119,7 @@ type CreateIssueOutput struct { URL string `json:"url"` } -func CreateIssue(_ context.Context, _ *mcp.CallToolRequest, in CreateIssueInput) (*mcp.CallToolResult, CreateIssueOutput, error) { +func createIssue(_ context.Context, _ *mcp.CallToolRequest, in CreateIssueInput) (*mcp.CallToolResult, CreateIssueOutput, error) { return nil, CreateIssueOutput{ Number: 102, URL: fmt.Sprintf("https://github.com/%s/issues/102", in.Repo), @@ -144,7 +136,7 @@ type AddLabelOutput struct { Labels []string `json:"currentLabels"` } -func AddLabel(_ context.Context, _ *mcp.CallToolRequest, in AddLabelInput) (*mcp.CallToolResult, AddLabelOutput, error) { +func addLabel(_ context.Context, _ *mcp.CallToolRequest, in AddLabelInput) (*mcp.CallToolResult, AddLabelOutput, error) { return nil, AddLabelOutput{ Labels: append([]string{"bug", "api"}, in.Labels...), }, nil @@ -170,7 +162,7 @@ type ListSecurityAlertsOutput struct { Alerts []SecurityAlert `json:"alerts"` } -func ListSecurityAlerts(_ context.Context, _ *mcp.CallToolRequest, in ListSecurityAlertsInput) (*mcp.CallToolResult, ListSecurityAlertsOutput, error) { +func listSecurityAlerts(_ context.Context, _ *mcp.CallToolRequest, in ListSecurityAlertsInput) (*mcp.CallToolResult, ListSecurityAlertsOutput, error) { return nil, ListSecurityAlertsOutput{ Alerts: []SecurityAlert{ {Number: 1, Rule: "sql-injection", Severity: "critical", State: "open", Path: "src/db/query.go"}, @@ -193,7 +185,7 @@ type GetAdvisoryOutput struct { PatchedIn string `json:"patchedIn"` } -func GetAdvisory(_ context.Context, _ *mcp.CallToolRequest, in GetAdvisoryInput) (*mcp.CallToolResult, GetAdvisoryOutput, error) { +func getAdvisory(_ context.Context, _ *mcp.CallToolRequest, in GetAdvisoryInput) (*mcp.CallToolResult, GetAdvisoryOutput, error) { return nil, GetAdvisoryOutput{ ID: in.AdvisoryID, Summary: "Remote code execution via crafted request payload", @@ -224,7 +216,7 @@ type ListWorkflowRunsOutput struct { Runs []WorkflowRun `json:"runs"` } -func ListWorkflowRuns(_ context.Context, _ *mcp.CallToolRequest, in ListWorkflowRunsInput) (*mcp.CallToolResult, ListWorkflowRunsOutput, error) { +func listWorkflowRuns(_ context.Context, _ *mcp.CallToolRequest, in ListWorkflowRunsInput) (*mcp.CallToolResult, ListWorkflowRunsOutput, error) { return nil, ListWorkflowRunsOutput{ Runs: []WorkflowRun{ {ID: 5001, Workflow: "ci.yml", Status: "completed", Conclusion: "success", Branch: "main"}, @@ -246,7 +238,7 @@ type TriggerWorkflowOutput struct { URL string `json:"url"` } -func TriggerWorkflow(_ context.Context, _ *mcp.CallToolRequest, in TriggerWorkflowInput) (*mcp.CallToolResult, TriggerWorkflowOutput, error) { +func triggerWorkflow(_ context.Context, _ *mcp.CallToolRequest, in TriggerWorkflowInput) (*mcp.CallToolResult, TriggerWorkflowOutput, error) { return nil, TriggerWorkflowOutput{ RunID: 5004, URL: fmt.Sprintf("https://github.com/%s/actions/runs/5004", in.Repo), diff --git a/go/sdk/examples/server/model-optimized/README.md b/go/sdk/examples/server/model-optimized/README.md new file mode 100644 index 0000000..e5c82d4 --- /dev/null +++ b/go/sdk/examples/server/model-optimized/README.md @@ -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" + } + ] +} +``` diff --git a/go/sdk/examples/server/model-optimized/main.go b/go/sdk/examples/server/model-optimized/main.go new file mode 100644 index 0000000..fedaf66 --- /dev/null +++ b/go/sdk/examples/server/model-optimized/main.go @@ -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)) +} diff --git a/go/sdk/examples/server/model-optimized/tools.go b/go/sdk/examples/server/model-optimized/tools.go new file mode 100644 index 0000000..2936088 --- /dev/null +++ b/go/sdk/examples/server/model-optimized/tools.go @@ -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 +} diff --git a/go/sdk/examples/server/research/README.md b/go/sdk/examples/server/research/README.md new file mode 100644 index 0000000..9c55be3 --- /dev/null +++ b/go/sdk/examples/server/research/README.md @@ -0,0 +1,27 @@ +# Research Assistant + +A variant-aware MCP server that manages context budget by providing the same research tools (`search_papers`, `get_paper`, `summarize`) at different verbosity levels. + +**Pattern demonstrated:** Context budget management with description verbosity control. + +## Variants + +| Variant | Verbosity | Status | Use Case | +|---|---|---|---| +| `deep-research` | Multi-paragraph with usage examples | Stable | Literature reviews, large context windows | +| `quick-lookup` | Single-sentence descriptions | Stable | Fast Q&A, limited token budgets | +| `synthesis` | Balanced, moderate detail | Experimental | Report generation workflows | + +All three variants expose the same three tools with the same behavior — only the description detail level differs. + +## Custom Ranking + +Clients send a `"contextSize"` hint (`"verbose"`, `"compact"`, or `"standard"`). The ranking function matches the hint to the appropriate variant, falling back to priority order. + +## Run + +```bash +go run ./examples/server/research +``` + +The server listens on `http://localhost:8080`. diff --git a/go/sdk/examples/server/research/main.go b/go/sdk/examples/server/research/main.go new file mode 100644 index 0000000..51edf6c --- /dev/null +++ b/go/sdk/examples/server/research/main.go @@ -0,0 +1,139 @@ +// 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. +// +// Capability demonstrated: Context budget management, description verbosity. +// +// 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"}, nil) + 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"}, nil) + 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"}, nil) + 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)) +} diff --git a/go/sdk/examples/server/research/tools.go b/go/sdk/examples/server/research/tools.go new file mode 100644 index 0000000..46cf68d --- /dev/null +++ b/go/sdk/examples/server/research/tools.go @@ -0,0 +1,110 @@ +package main + +import ( + "context" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +type SearchPapersInput struct { + Query string `json:"query" jsonschema:"search query for academic papers"` + MaxResults int `json:"maxResults,omitempty" jsonschema:"maximum number of results (1-50, default: 10)"` + Year int `json:"year,omitempty" jsonschema:"filter by publication year"` + Field string `json:"field,omitempty" jsonschema:"filter by field: cs, math, physics, biology, medicine"` +} + +type Paper struct { + ID string `json:"id"` + Title string `json:"title"` + Authors []string `json:"authors"` + Year int `json:"year"` + Abstract string `json:"abstract"` + Citations int `json:"citations"` +} + +type SearchPapersOutput struct { + Papers []Paper `json:"papers"` + TotalFound int `json:"totalFound"` +} + +func searchPapers(_ context.Context, _ *mcp.CallToolRequest, in SearchPapersInput) (*mcp.CallToolResult, SearchPapersOutput, error) { + return nil, SearchPapersOutput{ + Papers: []Paper{ + { + ID: "arxiv:2301.01234", + Title: "Attention Is All You Need: A Comprehensive Survey", + Authors: []string{"Smith, J.", "Chen, L.", "Kumar, A."}, + Year: 2024, + Abstract: "We present a comprehensive survey of transformer architectures and their applications across natural language processing, computer vision, and scientific computing.", + Citations: 342, + }, + { + ID: "arxiv:2302.05678", + Title: "Scaling Laws for Neural Language Models", + Authors: []string{"Johnson, R.", "Williams, K."}, + Year: 2024, + Abstract: "This paper investigates empirical scaling laws governing the performance of language models as a function of model size, dataset size, and compute budget.", + Citations: 189, + }, + }, + TotalFound: 1247, + }, nil +} + +type GetPaperInput struct { + ID string `json:"id" jsonschema:"paper identifier (e.g. arxiv:2301.01234, doi:10.1234/5678)"` +} + +type PaperDetail struct { + ID string `json:"id"` + Title string `json:"title"` + Authors []string `json:"authors"` + Year int `json:"year"` + Abstract string `json:"abstract"` + Citations int `json:"citations"` + References []string `json:"references"` + Keywords []string `json:"keywords"` + Venue string `json:"venue"` +} + +type GetPaperOutput struct { + Paper PaperDetail `json:"paper"` +} + +func getPaper(_ context.Context, _ *mcp.CallToolRequest, in GetPaperInput) (*mcp.CallToolResult, GetPaperOutput, error) { + return nil, GetPaperOutput{ + Paper: PaperDetail{ + ID: in.ID, + Title: "Attention Is All You Need: A Comprehensive Survey", + Authors: []string{"Smith, J.", "Chen, L.", "Kumar, A."}, + Year: 2024, + Abstract: "We present a comprehensive survey of transformer architectures and their applications across natural language processing, computer vision, and scientific computing.", + Citations: 342, + References: []string{"arxiv:1706.03762", "arxiv:1810.04805", "arxiv:2005.14165"}, + Keywords: []string{"transformers", "attention mechanisms", "deep learning", "survey"}, + Venue: "NeurIPS 2024", + }, + }, nil +} + +type SummarizeInput struct { + Text string `json:"text" jsonschema:"text to summarize"` + Style string `json:"style,omitempty" jsonschema:"summary style: abstract, bullet-points, executive (default: abstract)"` + Length string `json:"length,omitempty" jsonschema:"summary length: short, medium, long (default: medium)"` +} + +type SummarizeOutput struct { + Summary string `json:"summary"` + Style string `json:"style"` +} + +func summarize(_ context.Context, _ *mcp.CallToolRequest, in SummarizeInput) (*mcp.CallToolResult, SummarizeOutput, error) { + style := in.Style + if style == "" { + style = "abstract" + } + return nil, SummarizeOutput{ + Summary: "This paper presents key findings in the field, demonstrating significant improvements over prior work through novel methodology and comprehensive evaluation across multiple benchmarks.", + Style: style, + }, nil +} diff --git a/go/sdk/examples/server/trading/README.md b/go/sdk/examples/server/trading/README.md new file mode 100644 index 0000000..667596f --- /dev/null +++ b/go/sdk/examples/server/trading/README.md @@ -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`. diff --git a/go/sdk/examples/server/trading/main.go b/go/sdk/examples/server/trading/main.go new file mode 100644 index 0000000..cd392ed --- /dev/null +++ b/go/sdk/examples/server/trading/main.go @@ -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)) +} diff --git a/go/sdk/examples/server/trading/tools.go b/go/sdk/examples/server/trading/tools.go new file mode 100644 index 0000000..bc22099 --- /dev/null +++ b/go/sdk/examples/server/trading/tools.go @@ -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 +} diff --git a/go/sdk/examples/server/variants-stdio/main.go b/go/sdk/examples/server/variants-stdio/main.go new file mode 100644 index 0000000..c0df32e --- /dev/null +++ b/go/sdk/examples/server/variants-stdio/main.go @@ -0,0 +1,47 @@ +// Example: Minimal variant-aware MCP server over stdio. Registers a single +// variant with one tool to demonstrate the simplest possible setup. +// +// Run over stdio: +// +// go run ./examples/server/variants-stdio +package main + +import ( + "context" + "log" + + "github.com/modelcontextprotocol/go-sdk/mcp" + + "github.com/modelcontextprotocol/experimental-ext-variants/go/sdk/variants" +) + +type GreetInput struct { + Name string `json:"name" jsonschema:"name to greet"` +} + +type GreetOutput struct { + Message string `json:"message"` +} + +func greet(_ context.Context, _ *mcp.CallToolRequest, in GreetInput) (*mcp.CallToolResult, GreetOutput, error) { + return nil, GreetOutput{Message: "Hello, " + in.Name + "!"}, nil +} + +func main() { + inner := mcp.NewServer(&mcp.Implementation{Name: "greeter", Version: "v1.0.0"}, nil) + mcp.AddTool(inner, &mcp.Tool{ + Name: "greet", + Description: "Greet someone by name", + }, greet) + + vs := variants.NewServer(&mcp.Implementation{Name: "greeter", Version: "v1.0.0"}). + WithVariant(variants.ServerVariant{ + ID: "default", + Description: "Default greeting variant.", + Status: variants.Stable, + }, inner, 0) + + if err := vs.Run(context.Background(), &mcp.StdioTransport{}); err != nil { + log.Fatal(err) + } +} diff --git a/go/sdk/examples/server/variants/main.go b/go/sdk/examples/server/variants/main.go deleted file mode 100644 index 812bf32..0000000 --- a/go/sdk/examples/server/variants/main.go +++ /dev/null @@ -1,108 +0,0 @@ -// Example: A developer productivity platform (à la GitHub) that exposes -// different tool sets for different agent types using MCP server variants. -// -// Variants: -// - code-review: PR operations, diffs, and review comments -// - project-management: issue tracking, labels, and assignments -// - security-readonly: security scanning alerts and advisories (read-only) -// - ci-automation: CI/CD workflow management and dispatch -// -// Run over stdio: -// -// go run . -package main - -import ( - "context" - "log" - - "github.com/modelcontextprotocol/go-sdk/mcp" - - "github.com/modelcontextprotocol/experimental-ext-variants/go/sdk/examples/server/exampletools" - "github.com/modelcontextprotocol/experimental-ext-variants/go/sdk/variants" -) - -func main() { - // Code review variant: PR-focused tools for code review agents - codeReviewServer := mcp.NewServer(&mcp.Implementation{Name: "devplatform", Version: "v1.0.0"}, nil) - mcp.AddTool(codeReviewServer, &mcp.Tool{ - Name: "list_pull_requests", - Description: "List open pull requests, optionally filtered by author", - }, exampletools.ListPullRequests) - mcp.AddTool(codeReviewServer, &mcp.Tool{ - Name: "get_diff", - Description: "Get the diff for a pull request, including changed files and line counts", - }, exampletools.GetDiff) - mcp.AddTool(codeReviewServer, &mcp.Tool{ - Name: "add_review_comment", - Description: "Post a review comment on a specific line of a pull request", - }, exampletools.AddReviewComment) - - // Project management variant: issue-focused tools for PM agents - pmServer := mcp.NewServer(&mcp.Implementation{Name: "devplatform", Version: "v1.0.0"}, nil) - mcp.AddTool(pmServer, &mcp.Tool{ - Name: "list_issues", - Description: "List issues, optionally filtered by state and labels", - }, exampletools.ListIssues) - mcp.AddTool(pmServer, &mcp.Tool{ - Name: "create_issue", - Description: "Create a new issue with title, body, and optional labels", - }, exampletools.CreateIssue) - mcp.AddTool(pmServer, &mcp.Tool{ - Name: "add_label", - Description: "Add labels to an existing issue", - }, exampletools.AddLabel) - - // Security variant: read-only security scanning tools - securityServer := mcp.NewServer(&mcp.Implementation{Name: "devplatform", Version: "v1.0.0"}, nil) - mcp.AddTool(securityServer, &mcp.Tool{ - Name: "list_security_alerts", - Description: "List code scanning alerts for a repository", - }, exampletools.ListSecurityAlerts) - mcp.AddTool(securityServer, &mcp.Tool{ - Name: "get_advisory", - Description: "Get details of a security advisory", - }, exampletools.GetAdvisory) - - // CI/CD variant: workflow management tools for automation agents - ciServer := mcp.NewServer(&mcp.Implementation{Name: "devplatform", Version: "v1.0.0"}, nil) - mcp.AddTool(ciServer, &mcp.Tool{ - Name: "list_workflow_runs", - Description: "List recent workflow runs for a repository", - }, exampletools.ListWorkflowRuns) - mcp.AddTool(ciServer, &mcp.Tool{ - Name: "trigger_workflow", - Description: "Trigger a workflow dispatch event", - }, exampletools.TriggerWorkflow) - - vs := variants.NewServer(&mcp.Implementation{Name: "devplatform", Version: "v1.0.0"}). - WithVariant(variants.ServerVariant{ - ID: "code-review", - Description: "Pull request and code review operations. Includes diff viewing, review comments, approval workflows, and merge controls.", - Hints: map[string]string{"domain": "code-review", "accessLevel": "read-write"}, - Status: variants.Stable, - }, codeReviewServer, 0). - WithVariant(variants.ServerVariant{ - ID: "project-management", - Description: "Issue and project tracking operations. Includes issue CRUD, labels, milestones, assignments, and project boards.", - Hints: map[string]string{"domain": "project-management", "accessLevel": "read-write"}, - Status: variants.Stable, - }, pmServer, 1). - WithVariant(variants.ServerVariant{ - ID: "security-readonly", - Description: "Security scanning and vulnerability management. Read-only access to code scanning alerts, secret detection, and security advisories.", - Hints: map[string]string{"domain": "security", "accessLevel": "readonly"}, - Status: variants.Stable, - }, securityServer, 2). - WithVariant(variants.ServerVariant{ - ID: "ci-automation", - Description: "CI/CD workflow management. Trigger runs, monitor jobs, manage deployments designed for automation agents.", - Hints: map[string]string{"domain": "ci-cd", "accessLevel": "automation"}, - Status: variants.Stable, - }, ciServer, 3) - - ctx := context.Background() - if err := vs.Run(ctx, &mcp.StdioTransport{}); err != nil { - log.Fatal(err) - } -}