feat: add deployment example, workflow example, and MCP benchmarks (#2877)

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 <noreply@anthropic.com>
This commit is contained in:
Asim Aslam
2026-03-04 11:40:10 +00:00
committed by GitHub
co-authored by Claude
parent 870b540922
commit c800dd3729
9 changed files with 953 additions and 3 deletions
+17 -3
View File
@@ -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
+13
View File
@@ -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"]
+13
View File
@@ -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"]
+116
View File
@@ -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
+65
View File
@@ -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"
+10
View File
@@ -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.
+67
View File
@@ -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.
+393
View File
@@ -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)
}
}
+259
View File
@@ -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"
}
}