From c800dd37296c441b77c1e704c31e10311099ad46 Mon Sep 17 00:00:00 2001 From: Asim Aslam Date: Wed, 4 Mar 2026 11:40:10 +0000 Subject: [PATCH] feat: add deployment example, workflow example, and MCP benchmarks (#2877) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docker Compose deployment example (examples/deployment/): - docker-compose.yml with Consul, MCP gateway, Jaeger tracing - Dockerfile and Dockerfile.gateway for multi-stage builds - README with architecture diagram and customization guide Cross-service workflow example (examples/mcp/workflow/): - Inventory, Orders, Notifications services - Shows agents orchestrating multi-step workflows from natural language - Stock check → reserve → order → notify in a single agent conversation MCP gateway benchmark suite (gateway/mcp/benchmark_test.go): - ListTools: ~20μs (10 tools), ~48μs (100 tools) - Tool lookup: ~19ns (zero-alloc, scales to 500+ tools) - Auth inspect: ~7ns, scope check: ~16ns - Rate limiter: ~111ns per check - JSON encode/decode: ~1.5-2μs per tool Updated examples README with new examples index. https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc Co-authored-by: Claude --- examples/README.md | 20 +- examples/deployment/Dockerfile | 13 + examples/deployment/Dockerfile.gateway | 13 + examples/deployment/README.md | 116 ++++++++ examples/deployment/docker-compose.yml | 65 ++++ examples/mcp/README.md | 10 + examples/mcp/workflow/README.md | 67 +++++ examples/mcp/workflow/main.go | 393 +++++++++++++++++++++++++ gateway/mcp/benchmark_test.go | 259 ++++++++++++++++ 9 files changed, 953 insertions(+), 3 deletions(-) create mode 100644 examples/deployment/Dockerfile create mode 100644 examples/deployment/Dockerfile.gateway create mode 100644 examples/deployment/README.md create mode 100644 examples/deployment/docker-compose.yml create mode 100644 examples/mcp/workflow/README.md create mode 100644 examples/mcp/workflow/main.go create mode 100644 gateway/mcp/benchmark_test.go diff --git a/examples/README.md b/examples/README.md index b0d186bb..1ddec25a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -47,13 +47,27 @@ cd multi-service go run . ``` -## Coming Soon +### [deployment](./deployment/) +Docker Compose deployment with MCP gateway, Consul registry, and Jaeger tracing: +- Production-like architecture in one `docker-compose up` +- Standalone MCP gateway connected to service registry +- Distributed tracing with OpenTelemetry + Jaeger -The following examples are planned: +### MCP Examples + +See the [mcp/](./mcp/) directory for AI agent integration examples: +- **[hello](./mcp/hello/)** - Minimal MCP service (start here) +- **[crud](./mcp/crud/)** - CRUD contact book with full agent documentation +- **[workflow](./mcp/workflow/)** - Cross-service orchestration via AI agents +- **[documented](./mcp/documented/)** - All MCP features with auth scopes + +### [agent-demo](./agent-demo/) +Multi-service project management app (Projects, Tasks, Team) with seed data and agent playground integration. + +## Coming Soon - **pubsub-events** - Event-driven architecture with NATS - **grpc-integration** - Using go-micro with gRPC -- **production-ready** - Complete production-grade service with observability ## Prerequisites diff --git a/examples/deployment/Dockerfile b/examples/deployment/Dockerfile new file mode 100644 index 00000000..a77679e2 --- /dev/null +++ b/examples/deployment/Dockerfile @@ -0,0 +1,13 @@ +# Multi-stage build for a go-micro service +FROM golang:1.22-alpine AS builder + +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 go build -o /service . + +FROM alpine:3.19 +RUN apk --no-cache add ca-certificates +COPY --from=builder /service /service +ENTRYPOINT ["/service"] diff --git a/examples/deployment/Dockerfile.gateway b/examples/deployment/Dockerfile.gateway new file mode 100644 index 00000000..5cd9acfa --- /dev/null +++ b/examples/deployment/Dockerfile.gateway @@ -0,0 +1,13 @@ +# Standalone MCP gateway +FROM golang:1.22-alpine AS builder + +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 go build -o /gateway ./cmd/gateway + +FROM alpine:3.19 +RUN apk --no-cache add ca-certificates +COPY --from=builder /gateway /gateway +ENTRYPOINT ["/gateway"] diff --git a/examples/deployment/README.md b/examples/deployment/README.md new file mode 100644 index 00000000..658e7b67 --- /dev/null +++ b/examples/deployment/README.md @@ -0,0 +1,116 @@ +# Docker Compose Deployment Example + +Run a go-micro service with MCP gateway, service registry, and distributed tracing in one command. + +## Architecture + +``` +┌─────────┐ discover ┌──────────┐ RPC ┌─────────┐ +│ Agent │ ─────────────→ │ MCP │ ──────────→ │ Your │ +│ (Claude) │ MCP :3001 │ Gateway │ │ Service │ +└─────────┘ └──────────┘ └─────────┘ + │ │ + ▼ ▼ + ┌──────────┐ ┌──────────┐ + │ Consul │ │ Jaeger │ + │ Registry │ │ Tracing │ + │ :8500 │ │ :16686 │ + └──────────┘ └──────────┘ +``` + +## Quick Start + +```bash +docker-compose up +``` + +## Endpoints + +| Service | URL | +|---------|-----| +| MCP Tools | http://localhost:3001/mcp/tools | +| Consul UI | http://localhost:8500 | +| Jaeger UI | http://localhost:16686 | +| Service RPC | http://localhost:9090 | + +## Test + +```bash +# List MCP tools +curl http://localhost:3001/mcp/tools | jq + +# Call a tool +curl -X POST http://localhost:3001/mcp/call \ + -H 'Content-Type: application/json' \ + -d '{"tool": "myservice.Handler.Method", "arguments": {"key": "value"}}' + +# View traces in Jaeger +open http://localhost:16686 +``` + +## Connect Claude Code + +```bash +# Claude Code can connect to the running MCP gateway +# Add to your Claude Code MCP settings: +``` + +```json +{ + "mcpServers": { + "my-services": { + "url": "http://localhost:3001/mcp" + } + } +} +``` + +## Customizing + +### Add Your Service + +Replace the `app` service's build context with your service directory: + +```yaml +app: + build: + context: ../path/to/your/service + dockerfile: Dockerfile +``` + +### Add More Services + +```yaml +users: + build: ./users + environment: + MICRO_REGISTRY: consul + MICRO_REGISTRY_ADDRESS: consul:8500 + +orders: + build: ./orders + environment: + MICRO_REGISTRY: consul + MICRO_REGISTRY_ADDRESS: consul:8500 +``` + +All services register with Consul. The MCP gateway discovers them automatically. + +### Add Redis Cache + +```yaml +redis: + image: redis:7-alpine + ports: + - "6379:6379" +``` + +Then set `MICRO_CACHE_ADDRESS=redis:6379` on your service. + +### Production Considerations + +- Add health checks to each service +- Use named volumes for Consul data persistence +- Configure rate limiting on the MCP gateway +- Set up TLS between services +- Use secrets management for API keys diff --git a/examples/deployment/docker-compose.yml b/examples/deployment/docker-compose.yml new file mode 100644 index 00000000..4da7b7eb --- /dev/null +++ b/examples/deployment/docker-compose.yml @@ -0,0 +1,65 @@ +# Go Micro + MCP Gateway deployment with Docker Compose +# +# This runs: +# 1. Consul — service registry (discovery) +# 2. App — your go-micro service(s) +# 3. MCP Gateway — standalone MCP gateway connected to Consul +# 4. Jaeger — distributed tracing UI +# +# Usage: +# docker-compose up +# +# Endpoints: +# MCP Tools: http://localhost:3001/mcp/tools +# Consul UI: http://localhost:8500 +# Jaeger UI: http://localhost:16686 +# Service: http://localhost:9090 (RPC) + +services: + # --- Service Registry --- + consul: + image: consul:1.15 + ports: + - "8500:8500" + command: agent -server -bootstrap-expect=1 -ui -client=0.0.0.0 + + # --- Your Go Micro Service --- + app: + build: + context: . + dockerfile: Dockerfile + ports: + - "9090:9090" + environment: + MICRO_REGISTRY: consul + MICRO_REGISTRY_ADDRESS: consul:8500 + MICRO_SERVER_ADDRESS: :9090 + depends_on: + - consul + restart: unless-stopped + + # --- MCP Gateway (standalone) --- + mcp-gateway: + build: + context: . + dockerfile: Dockerfile.gateway + ports: + - "3001:3001" + environment: + MICRO_REGISTRY: consul + MICRO_REGISTRY_ADDRESS: consul:8500 + MCP_ADDRESS: :3001 + OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4318 + depends_on: + - consul + - app + restart: unless-stopped + + # --- Tracing --- + jaeger: + image: jaegertracing/all-in-one:1.53 + ports: + - "16686:16686" # UI + - "4318:4318" # OTLP HTTP + environment: + COLLECTOR_OTLP_ENABLED: "true" diff --git a/examples/mcp/README.md b/examples/mcp/README.md index bc57ac32..3dbff98f 100644 --- a/examples/mcp/README.md +++ b/examples/mcp/README.md @@ -29,6 +29,16 @@ cd crud go run main.go ``` +### [workflow](./workflow/) - Cross-Service Orchestration + +Three services (Inventory, Orders, Notifications) showing how an AI agent orchestrates multi-step workflows: search products, check stock, reserve inventory, place order, send confirmation — all from a single natural language request. + +**Run it:** +```bash +cd workflow +go run main.go +``` + ### [documented](./documented/) - Full-Featured Example Complete example showing all MCP features with a user service. diff --git a/examples/mcp/workflow/README.md b/examples/mcp/workflow/README.md new file mode 100644 index 00000000..f1b73202 --- /dev/null +++ b/examples/mcp/workflow/README.md @@ -0,0 +1,67 @@ +# Workflow Example: Cross-Service Orchestration + +An e-commerce scenario with three services (Inventory, Orders, Notifications) that demonstrates how AI agents orchestrate multi-step workflows across services — no glue code, no workflow engine. + +## The Workflow + +When a user says _"Order a ThinkPad for alice and send her a confirmation"_, the agent figures out the steps: + +``` +1. InventoryService.Search → Find the product +2. InventoryService.CheckStock → Verify availability +3. InventoryService.ReserveStock → Decrement inventory +4. OrderService.PlaceOrder → Create the order +5. NotificationService.Send → Email confirmation +``` + +No code connects these steps — the agent reads the tool descriptions and chains the calls itself. + +## Run + +```bash +go run . +``` + +## Services + +| Service | Tools | Purpose | +|---------|-------|---------| +| InventoryService | Search, CheckStock, ReserveStock | Product catalog and stock management | +| OrderService | PlaceOrder, GetOrder, ListOrders | Order creation and lookup | +| NotificationService | Send, List | Email/SMS/Slack notifications | + +## Example Prompts + +Try these with Claude Code (`micro mcp serve`) or any MCP-compatible agent: + +- "What laptops do you have in stock?" +- "Order a ThinkPad for alice@example.com and send her a confirmation" +- "Check if 'The Go Programming Language' is available" (it's out of stock!) +- "Order 3 Go Gopher t-shirts for bob@example.com, reserve the stock, and notify him via Slack" +- "Show me all orders and notifications for alice" + +## Why This Matters + +Traditional approach: +```go +// 50+ lines of glue code wiring services together +func handleOrder(req OrderRequest) { + product, err := inventoryClient.CheckStock(req.SKU) + if err != nil { ... } + if product.InStock < req.Quantity { ... } + _, err = inventoryClient.ReserveStock(req.SKU, req.Quantity) + if err != nil { ... } + order, err := orderClient.PlaceOrder(...) + if err != nil { ... } + _, err = notificationClient.Send(...) + // ... +} +``` + +Agent approach: +``` +User: "Order a ThinkPad for alice and confirm via email" +Agent: [reads tool descriptions, chains 5 calls, handles the out-of-stock case] +``` + +The agent handles the orchestration. You just write the individual services with good documentation. diff --git a/examples/mcp/workflow/main.go b/examples/mcp/workflow/main.go new file mode 100644 index 00000000..ef815ab9 --- /dev/null +++ b/examples/mcp/workflow/main.go @@ -0,0 +1,393 @@ +// Workflow example: cross-service orchestration via AI agents. +// +// This example runs three services (Inventory, Orders, Notifications) and +// demonstrates how an AI agent can orchestrate a multi-step workflow: +// +// 1. Check inventory for a product +// 2. Place an order if in stock +// 3. Send a confirmation notification +// +// The agent figures out the right sequence of calls on its own — no +// workflow engine, no glue code, just natural language. +// +// Run: +// +// go run . +// +// MCP tools: http://localhost:3001/mcp/tools +package main + +import ( + "context" + "fmt" + "log" + "strings" + "sync" + "time" + + "go-micro.dev/v5" + "go-micro.dev/v5/gateway/mcp" +) + +// --------------------------------------------------------------------------- +// Inventory service +// --------------------------------------------------------------------------- + +type Product struct { + SKU string `json:"sku" description:"Stock keeping unit identifier"` + Name string `json:"name" description:"Product name"` + Price float64 `json:"price" description:"Unit price in USD"` + InStock int `json:"in_stock" description:"Number of units available"` + Category string `json:"category" description:"Product category"` +} + +type CheckStockRequest struct { + SKU string `json:"sku" description:"Product SKU to check"` +} + +type CheckStockResponse struct { + Product *Product `json:"product" description:"Product details with current stock level"` +} + +type SearchProductsRequest struct { + Query string `json:"query" description:"Search term to match against product name or category"` + Category string `json:"category,omitempty" description:"Filter by category: electronics, clothing, books (optional)"` +} + +type SearchProductsResponse struct { + Products []*Product `json:"products" description:"Products matching the search criteria"` +} + +type ReserveStockRequest struct { + SKU string `json:"sku" description:"Product SKU to reserve"` + Quantity int `json:"quantity" description:"Number of units to reserve"` +} + +type ReserveStockResponse struct { + Reserved bool `json:"reserved" description:"True if stock was successfully reserved"` + Remaining int `json:"remaining" description:"Units remaining after reservation"` + Message string `json:"message" description:"Human-readable result message"` +} + +type InventoryService struct { + mu sync.RWMutex + products map[string]*Product +} + +// CheckStock returns the current stock level for a product. +// Use this before placing an order to verify availability. +// +// @example {"sku": "LAPTOP-001"} +func (s *InventoryService) CheckStock(ctx context.Context, req *CheckStockRequest, rsp *CheckStockResponse) error { + s.mu.RLock() + defer s.mu.RUnlock() + p, ok := s.products[req.SKU] + if !ok { + return fmt.Errorf("product %s not found", req.SKU) + } + rsp.Product = p + return nil +} + +// Search finds products by name or category. Use this to help +// users find what they're looking for before checking stock. +// +// @example {"query": "laptop"} +func (s *InventoryService) Search(ctx context.Context, req *SearchProductsRequest, rsp *SearchProductsResponse) error { + s.mu.RLock() + defer s.mu.RUnlock() + q := strings.ToLower(req.Query) + for _, p := range s.products { + if req.Category != "" && !strings.EqualFold(p.Category, req.Category) { + continue + } + if q == "" || strings.Contains(strings.ToLower(p.Name), q) || strings.Contains(strings.ToLower(p.Category), q) { + rsp.Products = append(rsp.Products, p) + } + } + return nil +} + +// ReserveStock decrements inventory for a product. Call this after +// confirming stock is available. Returns an error if insufficient stock. +// +// @example {"sku": "LAPTOP-001", "quantity": 1} +func (s *InventoryService) ReserveStock(ctx context.Context, req *ReserveStockRequest, rsp *ReserveStockResponse) error { + s.mu.Lock() + defer s.mu.Unlock() + p, ok := s.products[req.SKU] + if !ok { + return fmt.Errorf("product %s not found", req.SKU) + } + if p.InStock < req.Quantity { + rsp.Reserved = false + rsp.Remaining = p.InStock + rsp.Message = fmt.Sprintf("insufficient stock: requested %d but only %d available", req.Quantity, p.InStock) + return nil + } + p.InStock -= req.Quantity + rsp.Reserved = true + rsp.Remaining = p.InStock + rsp.Message = fmt.Sprintf("reserved %d units of %s", req.Quantity, p.Name) + return nil +} + +// --------------------------------------------------------------------------- +// Orders service +// --------------------------------------------------------------------------- + +type Order struct { + ID string `json:"id" description:"Unique order identifier"` + Customer string `json:"customer" description:"Customer name or email"` + SKU string `json:"sku" description:"Product SKU ordered"` + Quantity int `json:"quantity" description:"Number of units"` + Total float64 `json:"total" description:"Total order amount in USD"` + Status string `json:"status" description:"Order status: pending, confirmed, shipped, delivered"` + CreatedAt time.Time `json:"created_at" description:"When the order was placed"` +} + +type PlaceOrderRequest struct { + Customer string `json:"customer" description:"Customer name or email (required)"` + SKU string `json:"sku" description:"Product SKU to order (required)"` + Quantity int `json:"quantity" description:"Number of units (required, must be positive)"` +} + +type PlaceOrderResponse struct { + Order *Order `json:"order" description:"The newly created order"` +} + +type GetOrderRequest struct { + ID string `json:"id" description:"Order ID to look up"` +} + +type GetOrderResponse struct { + Order *Order `json:"order" description:"The requested order"` +} + +type ListOrdersRequest struct { + Customer string `json:"customer,omitempty" description:"Filter by customer (optional)"` + Status string `json:"status,omitempty" description:"Filter by status (optional)"` +} + +type ListOrdersResponse struct { + Orders []*Order `json:"orders" description:"Matching orders"` +} + +type OrderService struct { + mu sync.RWMutex + orders map[string]*Order + nextID int + // In a real app this would be a client to the inventory service + inventory *InventoryService +} + +// PlaceOrder creates a new order. Stock must be reserved first via +// InventoryService.ReserveStock — this service does not check inventory. +// +// @example {"customer": "alice@example.com", "sku": "LAPTOP-001", "quantity": 1} +func (s *OrderService) PlaceOrder(ctx context.Context, req *PlaceOrderRequest, rsp *PlaceOrderResponse) error { + if req.Customer == "" { + return fmt.Errorf("customer is required") + } + if req.SKU == "" { + return fmt.Errorf("sku is required") + } + if req.Quantity <= 0 { + return fmt.Errorf("quantity must be positive") + } + + // Look up price + s.inventory.mu.RLock() + p, ok := s.inventory.products[req.SKU] + s.inventory.mu.RUnlock() + if !ok { + return fmt.Errorf("product %s not found", req.SKU) + } + + s.mu.Lock() + defer s.mu.Unlock() + s.nextID++ + order := &Order{ + ID: fmt.Sprintf("ORD-%04d", s.nextID), + Customer: req.Customer, + SKU: req.SKU, + Quantity: req.Quantity, + Total: p.Price * float64(req.Quantity), + Status: "confirmed", + CreatedAt: time.Now(), + } + s.orders[order.ID] = order + rsp.Order = order + return nil +} + +// GetOrder retrieves an order by ID. +// +// @example {"id": "ORD-0001"} +func (s *OrderService) GetOrder(ctx context.Context, req *GetOrderRequest, rsp *GetOrderResponse) error { + s.mu.RLock() + defer s.mu.RUnlock() + o, ok := s.orders[req.ID] + if !ok { + return fmt.Errorf("order %s not found", req.ID) + } + rsp.Order = o + return nil +} + +// ListOrders returns orders, optionally filtered by customer or status. +// +// @example {"customer": "alice@example.com"} +func (s *OrderService) ListOrders(ctx context.Context, req *ListOrdersRequest, rsp *ListOrdersResponse) error { + s.mu.RLock() + defer s.mu.RUnlock() + for _, o := range s.orders { + if req.Customer != "" && o.Customer != req.Customer { + continue + } + if req.Status != "" && o.Status != req.Status { + continue + } + rsp.Orders = append(rsp.Orders, o) + } + return nil +} + +// --------------------------------------------------------------------------- +// Notifications service +// --------------------------------------------------------------------------- + +type Notification struct { + ID string `json:"id" description:"Notification identifier"` + Recipient string `json:"recipient" description:"Who received the notification"` + Subject string `json:"subject" description:"Notification subject line"` + Body string `json:"body" description:"Notification body text"` + Channel string `json:"channel" description:"Delivery channel: email, sms, or slack"` + SentAt time.Time `json:"sent_at" description:"When the notification was sent"` +} + +type SendNotificationRequest struct { + Recipient string `json:"recipient" description:"Email address, phone number, or Slack handle"` + Subject string `json:"subject" description:"Subject line (required)"` + Body string `json:"body" description:"Message body (required)"` + Channel string `json:"channel,omitempty" description:"Channel: email (default), sms, or slack"` +} + +type SendNotificationResponse struct { + Notification *Notification `json:"notification" description:"The sent notification with delivery details"` +} + +type ListNotificationsRequest struct { + Recipient string `json:"recipient,omitempty" description:"Filter by recipient (optional)"` +} + +type ListNotificationsResponse struct { + Notifications []*Notification `json:"notifications" description:"Sent notifications"` +} + +type NotificationService struct { + mu sync.RWMutex + notifications []*Notification + nextID int +} + +// Send delivers a notification to a recipient via the specified channel. +// Use this to confirm orders, alert users, or send updates. +// Defaults to email if no channel is specified. +// +// @example {"recipient": "alice@example.com", "subject": "Order Confirmed", "body": "Your order ORD-0001 has been confirmed.", "channel": "email"} +func (s *NotificationService) Send(ctx context.Context, req *SendNotificationRequest, rsp *SendNotificationResponse) error { + if req.Recipient == "" { + return fmt.Errorf("recipient is required") + } + if req.Subject == "" { + return fmt.Errorf("subject is required") + } + if req.Body == "" { + return fmt.Errorf("body is required") + } + channel := req.Channel + if channel == "" { + channel = "email" + } + + s.mu.Lock() + defer s.mu.Unlock() + s.nextID++ + n := &Notification{ + ID: fmt.Sprintf("notif-%d", s.nextID), + Recipient: req.Recipient, + Subject: req.Subject, + Body: req.Body, + Channel: channel, + SentAt: time.Now(), + } + s.notifications = append(s.notifications, n) + rsp.Notification = n + return nil +} + +// List returns sent notifications, optionally filtered by recipient. +// +// @example {"recipient": "alice@example.com"} +func (s *NotificationService) List(ctx context.Context, req *ListNotificationsRequest, rsp *ListNotificationsResponse) error { + s.mu.RLock() + defer s.mu.RUnlock() + for _, n := range s.notifications { + if req.Recipient != "" && n.Recipient != req.Recipient { + continue + } + rsp.Notifications = append(rsp.Notifications, n) + } + return nil +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +func main() { + service := micro.New("shop", + micro.Address(":9090"), + mcp.WithMCP(":3001"), + ) + service.Init() + + inventory := &InventoryService{products: map[string]*Product{ + "LAPTOP-001": {SKU: "LAPTOP-001", Name: "ThinkPad X1 Carbon", Price: 1299.99, InStock: 15, Category: "electronics"}, + "LAPTOP-002": {SKU: "LAPTOP-002", Name: "MacBook Air M3", Price: 1099.00, InStock: 8, Category: "electronics"}, + "PHONE-001": {SKU: "PHONE-001", Name: "Pixel 8 Pro", Price: 899.00, InStock: 23, Category: "electronics"}, + "BOOK-001": {SKU: "BOOK-001", Name: "Designing Data-Intensive Applications", Price: 45.99, InStock: 50, Category: "books"}, + "BOOK-002": {SKU: "BOOK-002", Name: "The Go Programming Language", Price: 39.99, InStock: 0, Category: "books"}, + "SHIRT-001": {SKU: "SHIRT-001", Name: "Go Gopher T-Shirt", Price: 24.99, InStock: 100, Category: "clothing"}, + }} + + orders := &OrderService{ + orders: make(map[string]*Order), + inventory: inventory, + } + + notifications := &NotificationService{} + + service.Handle(inventory) + service.Handle(orders) + service.Handle(notifications) + + fmt.Println() + fmt.Println(" Shop Workflow Demo") + fmt.Println() + fmt.Println(" MCP Tools: http://localhost:3001/mcp/tools") + fmt.Println() + fmt.Println(" Try asking an agent:") + fmt.Println() + fmt.Println(" \"What laptops do you have in stock?\"") + fmt.Println(" \"Order a ThinkPad for alice@example.com and send her a confirmation\"") + fmt.Println(" \"Check if 'The Go Programming Language' is available\"") + fmt.Println(" \"Show me all orders for alice@example.com\"") + fmt.Println(" \"Order 3 Go Gopher t-shirts for bob@example.com, reserve the stock, and notify him\"") + fmt.Println() + + if err := service.Run(); err != nil { + log.Fatal(err) + } +} diff --git a/gateway/mcp/benchmark_test.go b/gateway/mcp/benchmark_test.go new file mode 100644 index 00000000..0f3bdd4a --- /dev/null +++ b/gateway/mcp/benchmark_test.go @@ -0,0 +1,259 @@ +package mcp + +import ( + "bytes" + "context" + "encoding/json" + "log" + "net/http" + "net/http/httptest" + "testing" + + "go-micro.dev/v5/auth" + "go-micro.dev/v5/client" + "go-micro.dev/v5/registry" +) + +// benchServer creates a Server with N pre-populated tools. +func benchServer(n int, opts Options) *Server { + if opts.Logger == nil { + opts.Logger = log.New(log.Writer(), "", 0) + } + if opts.Context == nil { + opts.Context = context.Background() + } + if opts.Client == nil { + opts.Client = client.DefaultClient + } + if opts.Registry == nil { + opts.Registry = registry.DefaultRegistry + } + + s := &Server{ + opts: opts, + tools: make(map[string]*Tool, n), + limiters: make(map[string]*rateLimiter), + } + + for i := 0; i < n; i++ { + name := toolName(i) + s.tools[name] = &Tool{ + Name: name, + Description: "Benchmark tool " + name, + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "id": map[string]interface{}{ + "type": "string", + "description": "Resource identifier", + }, + }, + "required": []interface{}{"id"}, + }, + Service: "bench", + Endpoint: "Handler.Method", + } + } + + return s +} + +func toolName(i int) string { + return "bench.Handler.Method" + string(rune('A'+i%26)) +} + +// --- Benchmarks --- + +// BenchmarkListTools measures tool listing throughput. +// This is the most common MCP operation — agents call it on every session start. +func BenchmarkListTools(b *testing.B) { + for _, numTools := range []int{10, 50, 100} { + b.Run(toolCountLabel(numTools), func(b *testing.B) { + s := benchServer(numTools, Options{}) + req := httptest.NewRequest("GET", "/mcp/tools", nil) + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + w := httptest.NewRecorder() + s.handleListTools(w, req) + if w.Code != http.StatusOK { + b.Fatalf("unexpected status %d", w.Code) + } + } + }) + } +} + +// BenchmarkListToolsParallel measures concurrent tool listing. +func BenchmarkListToolsParallel(b *testing.B) { + s := benchServer(50, Options{}) + req := httptest.NewRequest("GET", "/mcp/tools", nil) + + b.ResetTimer() + b.ReportAllocs() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + w := httptest.NewRecorder() + s.handleListTools(w, req) + } + }) +} + +// BenchmarkToolLookup measures tool name resolution from the tools map. +func BenchmarkToolLookup(b *testing.B) { + for _, numTools := range []int{10, 50, 100, 500} { + b.Run(toolCountLabel(numTools), func(b *testing.B) { + s := benchServer(numTools, Options{}) + name := toolName(numTools / 2) // look up a tool in the middle + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + s.toolsMu.RLock() + _, ok := s.tools[name] + s.toolsMu.RUnlock() + if !ok { + b.Fatal("tool not found") + } + } + }) + } +} + +// BenchmarkAuthInspect measures auth token inspection overhead. +func BenchmarkAuthInspect(b *testing.B) { + ma := &mockAuth{ + accounts: map[string]*auth.Account{ + "valid-token": { + ID: "bench-user", + Scopes: []string{"read", "write"}, + }, + }, + } + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + acc, err := ma.Inspect("valid-token") + if err != nil || acc.ID != "bench-user" { + b.Fatal("unexpected result") + } + } +} + +// BenchmarkScopeCheck measures scope validation overhead per tool call. +func BenchmarkScopeCheck(b *testing.B) { + accountScopes := []string{"users:read", "users:write", "orders:read", "admin"} + requiredScopes := []string{"users:write"} + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + hasScope(accountScopes, requiredScopes) + } +} + +// BenchmarkAuditRecord measures audit record creation overhead. +func BenchmarkAuditRecord(b *testing.B) { + var records int + s := benchServer(10, Options{ + AuditFunc: func(r AuditRecord) { + records++ + }, + }) + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + s.opts.AuditFunc(AuditRecord{ + TraceID: "trace-123", + Tool: "bench.Handler.MethodA", + AccountID: "user-1", + Allowed: true, + }) + } +} + +// BenchmarkRateLimiter measures rate limiter check overhead. +func BenchmarkRateLimiter(b *testing.B) { + s := benchServer(10, Options{ + RateLimit: &RateLimitConfig{ + RequestsPerSecond: 1000000, // Very high so it doesn't block + Burst: 1000000, + }, + }) + // Initialize limiters for tools + for name := range s.tools { + s.limiters[name] = newRateLimiter(s.opts.RateLimit.RequestsPerSecond, s.opts.RateLimit.Burst) + } + name := toolName(0) + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + s.limitersMu.RLock() + l := s.limiters[name] + s.limitersMu.RUnlock() + l.Allow() + } +} + +// BenchmarkJSONEncodeTool measures JSON serialization of tool definitions. +func BenchmarkJSONEncodeTool(b *testing.B) { + tool := &Tool{ + Name: "myservice.Users.GetUser", + Description: "Retrieve a user by their unique ID. Returns the full profile.", + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "id": map[string]interface{}{ + "type": "string", + "description": "User ID in UUID format", + }, + }, + "required": []interface{}{"id"}, + }, + Scopes: []string{"users:read"}, + Service: "myservice", + Endpoint: "Users.GetUser", + } + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + var buf bytes.Buffer + json.NewEncoder(&buf).Encode(tool) + } +} + +// BenchmarkJSONDecodeCallRequest measures parsing of incoming tool call requests. +func BenchmarkJSONDecodeCallRequest(b *testing.B) { + body := []byte(`{"tool":"myservice.Users.GetUser","arguments":{"id":"user-123"}}`) + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + var req struct { + Tool string `json:"tool"` + Arguments map[string]interface{} `json:"arguments"` + } + json.Unmarshal(body, &req) + } +} + +// --- Helpers --- + +func toolCountLabel(n int) string { + switch { + case n >= 500: + return "500_tools" + case n >= 100: + return "100_tools" + case n >= 50: + return "50_tools" + default: + return "10_tools" + } +} +