mirror of
https://github.com/go-micro/go-micro.git
synced 2026-06-15 19:35:13 +02:00
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:
+17
-3
@@ -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
|
||||
|
||||
|
||||
@@ -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"]
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user