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
30
go/sdk/examples/server/github/README.md
Normal file
30
go/sdk/examples/server/github/README.md
Normal file
|
|
@ -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.
|
||||
133
go/sdk/examples/server/github/main.go
Normal file
133
go/sdk/examples/server/github/main.go
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
// 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
|
||||
// - 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:
|
||||
//
|
||||
// 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/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",
|
||||
}, listPullRequests)
|
||||
mcp.AddTool(codeReviewServer, &mcp.Tool{
|
||||
Name: "get_diff",
|
||||
Description: "Get the diff for a pull request, including changed files and line counts",
|
||||
}, getDiff)
|
||||
mcp.AddTool(codeReviewServer, &mcp.Tool{
|
||||
Name: "add_review_comment",
|
||||
Description: "Post a review comment on a specific line of a pull request",
|
||||
}, 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",
|
||||
}, listIssues)
|
||||
mcp.AddTool(pmServer, &mcp.Tool{
|
||||
Name: "create_issue",
|
||||
Description: "Create a new issue with title, body, and optional labels",
|
||||
}, createIssue)
|
||||
mcp.AddTool(pmServer, &mcp.Tool{
|
||||
Name: "add_label",
|
||||
Description: "Add labels to an existing issue",
|
||||
}, 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",
|
||||
}, listSecurityAlerts)
|
||||
mcp.AddTool(securityServer, &mcp.Tool{
|
||||
Name: "get_advisory",
|
||||
Description: "Get details of a security advisory",
|
||||
}, 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",
|
||||
}, listWorkflowRuns)
|
||||
mcp.AddTool(ciServer, &mcp.Tool{
|
||||
Name: "trigger_workflow",
|
||||
Description: "Trigger a workflow dispatch event",
|
||||
}, 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).
|
||||
// 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)
|
||||
|
||||
log.Println("Listening on :8080")
|
||||
log.Fatal(http.ListenAndServe(":8080", handler))
|
||||
}
|
||||
246
go/sdk/examples/server/github/tools.go
Normal file
246
go/sdk/examples/server/github/tools.go
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
// -- Code Review variant tools ------------------------------------------------
|
||||
|
||||
type ListPullRequestsInput struct {
|
||||
Repo string `json:"repo" jsonschema:"repository in owner/name format"`
|
||||
State string `json:"state,omitempty" jsonschema:"filter by state: open, closed, all (default: open)"`
|
||||
Author string `json:"author,omitempty" jsonschema:"filter by PR author username"`
|
||||
}
|
||||
|
||||
type PullRequest struct {
|
||||
Number int `json:"number"`
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
type ListPullRequestsOutput struct {
|
||||
PullRequests []PullRequest `json:"pullRequests"`
|
||||
}
|
||||
|
||||
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"},
|
||||
{Number: 38, Title: "Fix race condition in cache layer", Author: "bob", State: "open"},
|
||||
{Number: 35, Title: "Update dependencies to latest", Author: "alice", State: "open"},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type GetDiffInput struct {
|
||||
Repo string `json:"repo" jsonschema:"repository in owner/name format"`
|
||||
Number int `json:"number" jsonschema:"pull request number"`
|
||||
}
|
||||
|
||||
type GetDiffOutput struct {
|
||||
Diff string `json:"diff"`
|
||||
Files []string `json:"filesChanged"`
|
||||
Additions int `json:"additions"`
|
||||
Deletions int `json:"deletions"`
|
||||
}
|
||||
|
||||
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"},
|
||||
Additions: 12,
|
||||
Deletions: 3,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type AddReviewCommentInput struct {
|
||||
Repo string `json:"repo" jsonschema:"repository in owner/name format"`
|
||||
Number int `json:"number" jsonschema:"pull request number"`
|
||||
Path string `json:"path" jsonschema:"file path to comment on"`
|
||||
Line int `json:"line" jsonschema:"line number to comment on"`
|
||||
Body string `json:"body" jsonschema:"review comment body (markdown)"`
|
||||
}
|
||||
|
||||
type AddReviewCommentOutput struct {
|
||||
CommentID int `json:"commentId"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
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),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// -- Project Management variant tools -----------------------------------------
|
||||
|
||||
type ListIssuesInput struct {
|
||||
Repo string `json:"repo" jsonschema:"repository in owner/name format"`
|
||||
State string `json:"state,omitempty" jsonschema:"filter by state: open, closed, all (default: open)"`
|
||||
Labels []string `json:"labels,omitempty" jsonschema:"filter by label names"`
|
||||
}
|
||||
|
||||
type Issue struct {
|
||||
Number int `json:"number"`
|
||||
Title string `json:"title"`
|
||||
State string `json:"state"`
|
||||
Labels []string `json:"labels"`
|
||||
Assignee string `json:"assignee,omitempty"`
|
||||
}
|
||||
|
||||
type ListIssuesOutput struct {
|
||||
Issues []Issue `json:"issues"`
|
||||
}
|
||||
|
||||
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"},
|
||||
{Number: 98, Title: "Add support for webhook retries", State: "open", Labels: []string{"enhancement"}, Assignee: ""},
|
||||
{Number: 95, Title: "Document authentication flow", State: "open", Labels: []string{"documentation"}, Assignee: "carol"},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type CreateIssueInput struct {
|
||||
Repo string `json:"repo" jsonschema:"repository in owner/name format"`
|
||||
Title string `json:"title" jsonschema:"issue title"`
|
||||
Body string `json:"body" jsonschema:"issue body (markdown)"`
|
||||
Labels []string `json:"labels,omitempty" jsonschema:"labels to apply"`
|
||||
}
|
||||
|
||||
type CreateIssueOutput struct {
|
||||
Number int `json:"number"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
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),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type AddLabelInput struct {
|
||||
Repo string `json:"repo" jsonschema:"repository in owner/name format"`
|
||||
Number int `json:"number" jsonschema:"issue number"`
|
||||
Labels []string `json:"labels" jsonschema:"labels to add"`
|
||||
}
|
||||
|
||||
type AddLabelOutput struct {
|
||||
Labels []string `json:"currentLabels"`
|
||||
}
|
||||
|
||||
func addLabel(_ context.Context, _ *mcp.CallToolRequest, in AddLabelInput) (*mcp.CallToolResult, AddLabelOutput, error) {
|
||||
return nil, AddLabelOutput{
|
||||
Labels: append([]string{"bug", "api"}, in.Labels...),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// -- Security variant tools (read-only) ---------------------------------------
|
||||
|
||||
type ListSecurityAlertsInput struct {
|
||||
Repo string `json:"repo" jsonschema:"repository in owner/name format"`
|
||||
Severity string `json:"severity,omitempty" jsonschema:"filter by severity: critical, high, medium, low (default: all)"`
|
||||
State string `json:"state,omitempty" jsonschema:"filter by state: open, dismissed, fixed (default: open)"`
|
||||
}
|
||||
|
||||
type SecurityAlert struct {
|
||||
Number int `json:"number"`
|
||||
Rule string `json:"rule"`
|
||||
Severity string `json:"severity"`
|
||||
State string `json:"state"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
type ListSecurityAlertsOutput struct {
|
||||
Alerts []SecurityAlert `json:"alerts"`
|
||||
}
|
||||
|
||||
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"},
|
||||
{Number: 2, Rule: "hardcoded-secret", Severity: "high", State: "open", Path: "config/settings.go"},
|
||||
{Number: 3, Rule: "insecure-hash", Severity: "medium", State: "open", Path: "pkg/auth/hash.go"},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type GetAdvisoryInput struct {
|
||||
AdvisoryID string `json:"advisoryId" jsonschema:"security advisory identifier (e.g. GHSA-xxxx-xxxx-xxxx)"`
|
||||
}
|
||||
|
||||
type GetAdvisoryOutput struct {
|
||||
ID string `json:"id"`
|
||||
Summary string `json:"summary"`
|
||||
Severity string `json:"severity"`
|
||||
CVEs []string `json:"cves"`
|
||||
AffectedPkg string `json:"affectedPackage"`
|
||||
PatchedIn string `json:"patchedIn"`
|
||||
}
|
||||
|
||||
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",
|
||||
Severity: "critical",
|
||||
CVEs: []string{"CVE-2024-12345"},
|
||||
AffectedPkg: "example.com/server",
|
||||
PatchedIn: "v1.2.4",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// -- CI/CD variant tools ------------------------------------------------------
|
||||
|
||||
type ListWorkflowRunsInput struct {
|
||||
Repo string `json:"repo" jsonschema:"repository in owner/name format"`
|
||||
Workflow string `json:"workflow,omitempty" jsonschema:"filter by workflow name or filename"`
|
||||
Status string `json:"status,omitempty" jsonschema:"filter by status: completed, in_progress, queued (default: all)"`
|
||||
}
|
||||
|
||||
type WorkflowRun struct {
|
||||
ID int `json:"id"`
|
||||
Workflow string `json:"workflow"`
|
||||
Status string `json:"status"`
|
||||
Conclusion string `json:"conclusion,omitempty"`
|
||||
Branch string `json:"branch"`
|
||||
}
|
||||
|
||||
type ListWorkflowRunsOutput struct {
|
||||
Runs []WorkflowRun `json:"runs"`
|
||||
}
|
||||
|
||||
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"},
|
||||
{ID: 5002, Workflow: "ci.yml", Status: "in_progress", Branch: "feat/new-api"},
|
||||
{ID: 5003, Workflow: "deploy.yml", Status: "completed", Conclusion: "failure", Branch: "main"},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type TriggerWorkflowInput struct {
|
||||
Repo string `json:"repo" jsonschema:"repository in owner/name format"`
|
||||
Workflow string `json:"workflow" jsonschema:"workflow filename (e.g. ci.yml)"`
|
||||
Ref string `json:"ref" jsonschema:"git ref to run against (branch, tag, or SHA)"`
|
||||
Inputs map[string]string `json:"inputs,omitempty" jsonschema:"workflow input parameters"`
|
||||
}
|
||||
|
||||
type TriggerWorkflowOutput struct {
|
||||
RunID int `json:"runId"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
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),
|
||||
}, nil
|
||||
}
|
||||
Loading…
Reference in a new issue