chore: import upstream snapshot with attribution
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:40:33 +08:00
commit e071084ebe
982 changed files with 160368 additions and 0 deletions
+246
View File
@@ -0,0 +1,246 @@
# MCP Examples
Examples demonstrating Model Context Protocol (MCP) integration with go-micro.
## Examples
### [hello](./hello/) - Minimal Example ⭐ Start Here
The simplest possible MCP-enabled service. Perfect for learning the basics.
**What it shows:**
- Automatic documentation extraction from Go comments
- MCP gateway setup with 3 lines
- Ready for Claude Code
**Run it:**
```bash
cd hello
go run main.go
```
### [crud](./crud/) - CRUD Contact Book
A realistic service with create, read, update, delete, list, and search operations. Shows how to document a full API for agents with `@example` tags, `description` struct tags, validation errors, and partial updates.
**Run it:**
```bash
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
```
### [platform](./platform/) - Agent Platform Showcase
A complete platform (Users, Posts, Comments, Mail) mirroring [micro/blog](https://github.com/micro/blog). Shows how existing microservices become agent-accessible with zero code changes — agents can sign up, write posts, comment, tag, and send mail through natural language.
**Run it:**
```bash
cd platform
go run main.go
```
### [documented](./documented/) - Full-Featured Example
Complete example showing all MCP features with a user service.
**What it shows:**
- Multiple endpoints (GetUser, CreateUser)
- Rich documentation with examples
- Per-endpoint auth scopes via `server.WithEndpointScopes()`
- Pre-populated test data
- Production-ready patterns
**Run it:**
```bash
cd documented
go run main.go
```
## Quick Start
### 1. Write Your Service
Add Go doc comments to your handler methods:
```go
// SayHello greets a person by name. Returns a friendly greeting message.
//
// @example {"name": "Alice"}
func (g *Greeter) SayHello(ctx context.Context, req *HelloRequest, rsp *HelloResponse) error {
rsp.Message = "Hello " + req.Name + "!"
return nil
}
type HelloRequest struct {
Name string `json:"name" description:"Person's name to greet"`
}
```
### 2. Register Handler (Auto-Extracts Docs!)
```go
handler := service.Server().NewHandler(new(Greeter))
service.Server().Handle(handler)
```
### 3. Start MCP Gateway
```go
go mcp.ListenAndServe(":3000", mcp.Options{
Registry: service.Options().Registry,
})
```
## Testing
### HTTP API
```bash
# List tools
curl http://localhost:3000/mcp/tools | jq
# Call a tool
curl -X POST http://localhost:3000/mcp/call \
-H "Content-Type: application/json" \
-d '{
"tool": "greeter.Greeter.SayHello",
"input": {"name": "Alice"}
}' | jq
```
### Claude Code (Stdio)
Start MCP server:
```bash
micro mcp serve
```
Add to `~/.claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"my-services": {
"command": "micro",
"args": ["mcp", "serve"]
}
}
}
```
Restart Claude Code and ask Claude to use your services!
## Features
### ✅ Automatic Documentation Extraction
Just write Go comments - documentation is extracted automatically:
- **Go doc comments** → Tool descriptions
- **@example tags** → Example inputs for AI
- **Struct tags** → Parameter descriptions
### ✅ Multiple Transports
- **Stdio** - For Claude Code (recommended)
- **HTTP/SSE** - For web-based agents
### ✅ MCP Command Line
```bash
# Start MCP server
micro mcp serve # Stdio (for Claude Code)
micro mcp serve --address :3000 # HTTP/SSE (for web agents)
# List available tools
micro mcp list # Human-readable list
micro mcp list --json # JSON output
# Test a tool
micro mcp test <tool-name> '{"key": "value"}'
# Generate documentation
micro mcp docs # Markdown format
micro mcp docs --format json # JSON format
micro mcp docs --output tools.md # Save to file
# Export to different formats
micro mcp export langchain # Python LangChain tools
micro mcp export openapi # OpenAPI 3.0 spec
micro mcp export json # Raw JSON definitions
```
For detailed examples, see [CLI Examples](../../cmd/micro/mcp/EXAMPLES.md).
### ✅ Zero Configuration
- No manual tool registration
- No API wrappers
- No code generation
- Just write normal Go code!
### ✅ Per-Tool Auth Scopes
Declare required scopes when registering a handler:
```go
handler := service.Server().NewHandler(
new(BlogService),
server.WithEndpointScopes("Blog.Create", "blog:write"),
server.WithEndpointScopes("Blog.Delete", "blog:admin"),
)
```
Or define scopes at the gateway layer without changing services:
```go
mcp.Serve(mcp.Options{
Registry: reg,
Auth: authProvider,
Scopes: map[string][]string{
"blog.Blog.Create": {"blog:write"},
"blog.Blog.Delete": {"blog:admin"},
},
})
```
### ✅ Tracing, Rate Limiting & Audit Logging
Every tool call generates a trace ID that propagates through the RPC chain.
Configure rate limiting and audit logging at the gateway:
```go
mcp.Serve(mcp.Options{
Registry: reg,
Auth: authProvider,
RateLimit: &mcp.RateLimitConfig{
RequestsPerSecond: 10,
Burst: 20,
},
AuditFunc: func(r mcp.AuditRecord) {
log.Printf("[audit] trace=%s tool=%s account=%s allowed=%v",
r.TraceID, r.Tool, r.AccountID, r.Allowed)
},
})
```
## Documentation
- [Full MCP Documentation](../../internal/website/docs/mcp.md)
- [MCP Gateway Implementation](../../gateway/mcp/)
- [Documentation Guide](../../gateway/mcp/DOCUMENTATION.md)
- [Blog Post](../../internal/website/blog/2.md)
## Learn More
- [Model Context Protocol Spec](https://modelcontextprotocol.io/)
- [Go Micro Documentation](https://go-micro.dev)
+74
View File
@@ -0,0 +1,74 @@
# CRUD Contact Book Example
A complete CRUD service with MCP integration — the kind of service you'd actually build in production.
## What This Shows
- **6 operations**: Create, Get, Update, Delete, List, Search
- **Rich documentation**: Every handler has doc comments with `@example` tags
- **Struct tag descriptions**: All fields have `description` tags for agents
- **Input validation**: Required field checks with clear error messages
- **Partial updates**: Update only changes non-empty fields
- **Seed data**: Starts with 3 contacts so agents can explore immediately
## Run
```bash
go run .
```
## Test
```bash
# List all MCP tools
curl http://localhost:3001/mcp/tools | jq
# Create a contact
curl -X POST http://localhost:3001/mcp/call \
-H 'Content-Type: application/json' \
-d '{"tool": "contacts.Contacts.Create", "arguments": {"name": "Dave", "email": "dave@example.com"}}'
# Search contacts
curl -X POST http://localhost:3001/mcp/call \
-H 'Content-Type: application/json' \
-d '{"tool": "contacts.Contacts.Search", "arguments": {"query": "engineer"}}'
```
## Use with Claude Code
```bash
micro mcp serve
```
Then ask: "List all contacts and find the engineers."
## Key Patterns
### Doc Comments for Agents
```go
// Create adds a new contact to the book. Name and email are required.
//
// @example {"name": "Dave Wilson", "email": "dave@example.com", "role": "Engineer"}
func (h *Contacts) Create(ctx context.Context, req *CreateRequest, rsp *CreateResponse) error {
```
### Struct Tag Descriptions
```go
type Contact struct {
ID string `json:"id" description:"Unique contact identifier"`
Name string `json:"name" description:"Full name"`
Email string `json:"email" description:"Email address"`
}
```
### Partial Updates
Only update fields that are provided (non-empty), so agents can change one field without overwriting others:
```go
if req.Name != "" {
contact.Name = req.Name
}
```
+278
View File
@@ -0,0 +1,278 @@
// CRUD example: a contact book service with full MCP integration.
//
// This shows a realistic service with create, read, update, delete, and
// search operations, all automatically exposed as MCP tools with rich
// documentation for AI agents.
//
// Run:
//
// go run .
//
// MCP tools: http://localhost:3001/mcp/tools
// Test: curl http://localhost:3001/mcp/tools | jq
package main
import (
"context"
"fmt"
"log"
"strings"
"sync"
"go-micro.dev/v6"
"go-micro.dev/v6/gateway/mcp"
)
// --- Types ---
// Contact represents a person in the contact book.
type Contact struct {
ID string `json:"id" description:"Unique contact identifier"`
Name string `json:"name" description:"Full name"`
Email string `json:"email" description:"Email address"`
Phone string `json:"phone" description:"Phone number in E.164 format"`
Role string `json:"role" description:"Job title or role"`
Notes string `json:"notes" description:"Free-text notes about this contact"`
}
type CreateRequest struct {
Name string `json:"name" description:"Full name (required)"`
Email string `json:"email" description:"Email address (required)"`
Phone string `json:"phone" description:"Phone number"`
Role string `json:"role" description:"Job title or role"`
Notes string `json:"notes" description:"Free-text notes"`
}
type CreateResponse struct {
Contact *Contact `json:"contact" description:"The newly created contact"`
}
type GetRequest struct {
ID string `json:"id" description:"Contact ID to look up"`
}
type GetResponse struct {
Contact *Contact `json:"contact" description:"The requested contact"`
}
type UpdateRequest struct {
ID string `json:"id" description:"Contact ID to update (required)"`
Name string `json:"name" description:"New name (leave empty to keep current)"`
Email string `json:"email" description:"New email (leave empty to keep current)"`
Phone string `json:"phone" description:"New phone (leave empty to keep current)"`
Role string `json:"role" description:"New role (leave empty to keep current)"`
Notes string `json:"notes" description:"New notes (leave empty to keep current)"`
}
type UpdateResponse struct {
Contact *Contact `json:"contact" description:"The updated contact"`
}
type DeleteRequest struct {
ID string `json:"id" description:"Contact ID to delete"`
}
type DeleteResponse struct {
Deleted bool `json:"deleted" description:"True if the contact was deleted"`
}
type ListRequest struct {
}
type ListResponse struct {
Contacts []*Contact `json:"contacts" description:"All contacts in the book"`
}
type SearchRequest struct {
Query string `json:"query" description:"Search term to match against name, email, role, or notes"`
}
type SearchResponse struct {
Contacts []*Contact `json:"contacts" description:"Contacts matching the search query"`
}
// --- Handler ---
// Contacts manages a contact book with CRUD operations.
type Contacts struct {
mu sync.RWMutex
store map[string]*Contact
counter int
}
func NewContacts() *Contacts {
c := &Contacts{store: make(map[string]*Contact)}
// Seed with example data
c.store["c-1"] = &Contact{ID: "c-1", Name: "Alice Johnson", Email: "alice@example.com", Phone: "+1-555-0101", Role: "Engineer", Notes: "Backend team lead"}
c.store["c-2"] = &Contact{ID: "c-2", Name: "Bob Smith", Email: "bob@example.com", Phone: "+1-555-0102", Role: "Designer", Notes: "UI/UX specialist"}
c.store["c-3"] = &Contact{ID: "c-3", Name: "Carol Davis", Email: "carol@example.com", Phone: "+1-555-0103", Role: "PM", Notes: "Leads the platform team"}
c.counter = 3
return c
}
// Create adds a new contact to the book. Name and email are required.
//
// @example {"name": "Dave Wilson", "email": "dave@example.com", "role": "Engineer"}
func (h *Contacts) Create(ctx context.Context, req *CreateRequest, rsp *CreateResponse) error {
if req.Name == "" {
return fmt.Errorf("name is required")
}
if req.Email == "" {
return fmt.Errorf("email is required")
}
h.mu.Lock()
defer h.mu.Unlock()
h.counter++
id := fmt.Sprintf("c-%d", h.counter)
contact := &Contact{
ID: id,
Name: req.Name,
Email: req.Email,
Phone: req.Phone,
Role: req.Role,
Notes: req.Notes,
}
h.store[id] = contact
rsp.Contact = contact
return nil
}
// Get retrieves a single contact by ID.
//
// @example {"id": "c-1"}
func (h *Contacts) Get(ctx context.Context, req *GetRequest, rsp *GetResponse) error {
if req.ID == "" {
return fmt.Errorf("id is required")
}
h.mu.RLock()
defer h.mu.RUnlock()
contact, ok := h.store[req.ID]
if !ok {
return fmt.Errorf("contact %s not found", req.ID)
}
rsp.Contact = contact
return nil
}
// Update modifies an existing contact. Only non-empty fields are updated,
// so you can change just the email without affecting other fields.
//
// @example {"id": "c-1", "role": "Senior Engineer"}
func (h *Contacts) Update(ctx context.Context, req *UpdateRequest, rsp *UpdateResponse) error {
if req.ID == "" {
return fmt.Errorf("id is required")
}
h.mu.Lock()
defer h.mu.Unlock()
contact, ok := h.store[req.ID]
if !ok {
return fmt.Errorf("contact %s not found", req.ID)
}
if req.Name != "" {
contact.Name = req.Name
}
if req.Email != "" {
contact.Email = req.Email
}
if req.Phone != "" {
contact.Phone = req.Phone
}
if req.Role != "" {
contact.Role = req.Role
}
if req.Notes != "" {
contact.Notes = req.Notes
}
rsp.Contact = contact
return nil
}
// Delete removes a contact from the book permanently.
//
// @example {"id": "c-1"}
func (h *Contacts) Delete(ctx context.Context, req *DeleteRequest, rsp *DeleteResponse) error {
if req.ID == "" {
return fmt.Errorf("id is required")
}
h.mu.Lock()
defer h.mu.Unlock()
if _, ok := h.store[req.ID]; !ok {
return fmt.Errorf("contact %s not found", req.ID)
}
delete(h.store, req.ID)
rsp.Deleted = true
return nil
}
// List returns all contacts in the book.
//
// @example {}
func (h *Contacts) List(ctx context.Context, req *ListRequest, rsp *ListResponse) error {
h.mu.RLock()
defer h.mu.RUnlock()
for _, c := range h.store {
rsp.Contacts = append(rsp.Contacts, c)
}
return nil
}
// Search finds contacts matching a query string. Matches against name,
// email, role, and notes fields (case-insensitive).
//
// @example {"query": "engineer"}
func (h *Contacts) Search(ctx context.Context, req *SearchRequest, rsp *SearchResponse) error {
if req.Query == "" {
return fmt.Errorf("query is required")
}
h.mu.RLock()
defer h.mu.RUnlock()
q := strings.ToLower(req.Query)
for _, c := range h.store {
if strings.Contains(strings.ToLower(c.Name), q) ||
strings.Contains(strings.ToLower(c.Email), q) ||
strings.Contains(strings.ToLower(c.Role), q) ||
strings.Contains(strings.ToLower(c.Notes), q) {
rsp.Contacts = append(rsp.Contacts, c)
}
}
return nil
}
func main() {
service := micro.NewService("contacts",
micro.Address(":9010"),
mcp.WithMCP(":3001"),
)
service.Init()
if err := service.Handle(NewContacts()); err != nil {
log.Fatal(err)
}
fmt.Println("Contacts service running on :9010")
fmt.Println("MCP tools available at http://localhost:3001/mcp/tools")
fmt.Println()
fmt.Println("Try asking an AI agent:")
fmt.Println(" 'List all contacts'")
fmt.Println(" 'Find engineers in the contact book'")
fmt.Println(" 'Add a new contact for Eve at eve@example.com'")
fmt.Println(" 'Update Alice's role to Staff Engineer'")
if err := service.Run(); err != nil {
log.Fatal(err)
}
}
+306
View File
@@ -0,0 +1,306 @@
# Documented Service Example
This example demonstrates how to document your go-micro service handlers so that AI agents can understand them better. The MCP gateway parses Go comments and struct tags to generate rich tool descriptions.
## Documentation Features
### 1. **Go Doc Comments**
Standard Go documentation comments are used as the tool description:
```go
// GetUser retrieves a user by ID from the database.
//
// This endpoint fetches a user's complete profile including their name,
// email, and age. If the user doesn't exist, an error is returned.
func (u *Users) GetUser(ctx context.Context, req *GetUserRequest, rsp *GetUserResponse) error {
// ...
}
```
### 2. **JSDoc-Style Tags**
Use `@param`, `@return`, and `@example` tags for detailed documentation:
```go
// CreateUser creates a new user in the system.
//
// @param name {string} User's full name (required, 1-100 characters)
// @param email {string} User's email address (required, must be valid email format)
// @param age {number} User's age (optional, must be 0-150 if provided)
// @return {User} The newly created user with generated ID
// @example
// {
// "name": "Alice Smith",
// "email": "alice@example.com",
// "age": 30
// }
func (u *Users) CreateUser(ctx context.Context, req *CreateUserRequest, rsp *CreateUserResponse) error {
// ...
}
```
### 3. **Struct Tags**
Add `description` tags to struct fields for better schema:
```go
type User struct {
ID string `json:"id" description:"User's unique identifier (UUID format)"`
Name string `json:"name" description:"User's full name"`
Email string `json:"email" description:"User's email address"`
Age int `json:"age,omitempty" description:"User's age (optional)"`
}
```
## Running the Example
### 1. Start the Service
```bash
cd examples/mcp/documented
go run main.go
```
Output:
```
Users service starting...
Service: users
Endpoints:
- Users.GetUser
- Users.CreateUser
MCP Gateway: http://localhost:3000
```
### 2. Test MCP Tools
List available tools:
```bash
curl http://localhost:3000/mcp/tools | jq
```
You'll see rich descriptions:
```json
{
"tools": [
{
"name": "users.Users.GetUser",
"description": "GetUser retrieves a user by ID from the database",
"inputSchema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "User ID in UUID format (e.g., \"123e4567-e89b-12d3-a456-426614174000\")"
}
},
"required": ["id"],
"examples": [
"{\"id\": \"123e4567-e89b-12d3-a456-426614174000\"}"
]
}
},
{
"name": "users.Users.CreateUser",
"description": "CreateUser creates a new user in the system",
"inputSchema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "User's full name (required, 1-100 characters)"
},
"email": {
"type": "string",
"description": "User's email address (required, must be valid email format)"
},
"age": {
"type": "number",
"description": "User's age (optional, must be 0-150 if provided)"
}
},
"required": ["name", "email"],
"examples": [
"{\"name\": \"Alice Smith\", \"email\": \"alice@example.com\", \"age\": 30}"
]
}
}
]
}
```
### 3. Call a Tool
Get existing user:
```bash
curl -X POST http://localhost:3000/mcp/call \
-H "Content-Type: application/json" \
-d '{
"tool": "users.Users.GetUser",
"input": {"id": "user-1"}
}'
```
Create new user:
```bash
curl -X POST http://localhost:3000/mcp/call \
-H "Content-Type: application/json" \
-d '{
"tool": "users.Users.CreateUser",
"input": {
"name": "Alice Smith",
"email": "alice@example.com",
"age": 30
}
}'
```
### 4. Use with Claude Code
Add to your `claude_desktop_config.json`:
```json
{
"mcpServers": {
"users-service": {
"command": "go",
"args": ["run", "/path/to/examples/mcp/documented/main.go"]
}
}
}
```
Then in Claude Code, ask:
```
> You: "Show me user-1's profile"
Claude will:
1. See the GetUser tool with rich description
2. Understand it needs an "id" parameter (UUID format)
3. Call users.Users.GetUser with {"id": "user-1"}
4. Return the user profile
```
## Documentation Best Practices
### DO: Write Clear Descriptions
```go
// ✅ Good: Clear, explains what and why
// GetUser retrieves a user by ID from the database.
// Returns full profile including email, name, and preferences.
```
```go
// ❌ Bad: Vague, no context
// Get gets a user
```
### DO: Specify Parameter Constraints
```go
// ✅ Good: Specifies format and constraints
// @param id {string} User ID in UUID format (e.g., "123e4567-e89b-12d3-a456-426614174000")
// @param age {number} User's age (must be 0-150)
```
```go
// ❌ Bad: No constraints or format
// @param id {string} The ID
```
### DO: Provide Examples
```go
// ✅ Good: Real example agents can use
// @example
// {
// "name": "Alice Smith",
// "email": "alice@example.com",
// "age": 30
// }
```
```go
// ❌ Bad: No example
// (agents have to guess the format)
```
### DO: Use Descriptive Struct Tags
```go
// ✅ Good: Explains what the field is
type User struct {
ID string `json:"id" description:"User's unique identifier (UUID format)"`
}
```
```go
// ❌ Bad: No description
type User struct {
ID string `json:"id"`
}
```
## How It Works
1. **Go Doc Parsing**
- The MCP gateway reads your service's Go comments
- First line becomes the tool description
- Full comment becomes the detailed description
2. **JSDoc Tag Parsing**
- `@param` tags enhance parameter descriptions
- `@return` tags describe what the tool returns
- `@example` tags provide usage examples
3. **Struct Tag Reading**
- `description` tags add context to fields
- `json:"field,omitempty"` marks optional fields
- Used to generate JSON Schema for parameters
4. **Schema Generation**
- Combines parsed documentation with type information
- Creates rich JSON Schema for each tool
- Agents use this to understand how to call your service
## Impact on Agent Performance
### Without Documentation
```
Tool: users.Users.GetUser
Description: Call GetUser on users service
Parameters: { "id": "string" }
```
Agent thinks: *"What's an ID? What format? What if I pass the wrong thing?"*
### With Documentation
```
Tool: users.Users.GetUser
Description: Retrieves a user by ID from the database. Returns full profile
including email, name, and preferences.
Parameters:
- id (string, required): User ID in UUID format
Example: "123e4567-e89b-12d3-a456-426614174000"
Example:
{"id": "user-1"}
```
Agent thinks: *"I need a UUID format ID. I can use 'user-1' from the example!"*
**Result:** Agent calls your service correctly on the first try!
## Next Steps
- Document all your service handlers with clear descriptions
- Add `@param`, `@return`, and `@example` tags
- Use `description` tags in struct fields
- Test with Claude Code to see how agents understand your services
## License
Apache 2.0
+137
View File
@@ -0,0 +1,137 @@
// Package main demonstrates how to document your service handlers for better
// AI agent integration using endpoint metadata.
//
// Services register descriptions with their endpoints, and the MCP gateway
// reads these descriptions from the registry to generate rich tool descriptions.
package main
import (
"context"
"fmt"
"log"
"go-micro.dev/v6"
"go-micro.dev/v6/gateway/mcp"
"go-micro.dev/v6/server"
)
// User represents a user in the system
type User struct {
ID string `json:"id" description:"User's unique identifier (UUID format)"`
Name string `json:"name" description:"User's full name"`
Email string `json:"email" description:"User's email address"`
Age int `json:"age,omitempty" description:"User's age (optional)"`
}
// GetUserRequest is the request for getting a user
type GetUserRequest struct {
ID string `json:"id" description:"User ID to retrieve"`
}
// GetUserResponse is the response containing user data
type GetUserResponse struct {
User *User `json:"user" description:"The requested user object"`
}
// CreateUserRequest is the request for creating a user
type CreateUserRequest struct {
Name string `json:"name" description:"User's full name (required)"`
Email string `json:"email" description:"User's email address (required)"`
Age int `json:"age,omitempty" description:"User's age (optional)"`
}
// CreateUserResponse contains the newly created user
type CreateUserResponse struct {
User *User `json:"user" description:"The newly created user"`
}
// Users service handles user-related operations
type Users struct {
users map[string]*User
}
// GetUser retrieves a user by ID from the database. Returns full profile including email, name, and preferences. If the user doesn't exist, an error is returned.
//
// @example {"id": "user-1"}
func (u *Users) GetUser(ctx context.Context, req *GetUserRequest, rsp *GetUserResponse) error {
user, exists := u.users[req.ID]
if !exists {
return fmt.Errorf("user not found: %s", req.ID)
}
rsp.User = user
return nil
}
// CreateUser creates a new user in the system. Validates the user data and creates a new profile. Name and email are required fields, while age is optional. Email must be unique across all users.
//
// @example {"name": "Alice Smith", "email": "alice@example.com", "age": 30}
func (u *Users) CreateUser(ctx context.Context, req *CreateUserRequest, rsp *CreateUserResponse) error {
// Validate input
if req.Name == "" || req.Email == "" {
return fmt.Errorf("name and email are required")
}
// Generate ID (simplified for example)
id := fmt.Sprintf("user-%d", len(u.users)+1)
user := &User{
ID: id,
Name: req.Name,
Email: req.Email,
Age: req.Age,
}
u.users[id] = user
rsp.User = user
return nil
}
func main() {
// Create service
service := micro.NewService("users",
micro.Address(":9090"),
// Start MCP gateway alongside the service
mcp.WithMCP(":3000"),
)
service.Init()
// Register handler with pre-populated test data.
// Documentation is automatically extracted from method comments.
// Use WithEndpointScopes to declare required auth scopes per endpoint.
if err := service.Handle(
&Users{
users: map[string]*User{
"user-1": {ID: "user-1", Name: "John Doe", Email: "john@example.com", Age: 25},
"user-2": {ID: "user-2", Name: "Jane Smith", Email: "jane@example.com", Age: 30},
},
},
server.WithEndpointScopes("Users.GetUser", "users:read"),
server.WithEndpointScopes("Users.CreateUser", "users:write"),
); err != nil {
log.Fatal(err)
}
log.Println("Users service starting...")
log.Println("Service: users")
log.Println("Endpoints:")
log.Println(" - Users.GetUser")
log.Println(" - Users.CreateUser")
log.Println("MCP Gateway: http://localhost:3000")
log.Println("")
log.Println("Test with:")
log.Println(" curl http://localhost:3000/mcp/tools")
log.Println("")
log.Println("Or add to Claude Code:")
log.Println(` "users-service": {`)
log.Println(` "command": "micro",`)
log.Println(` "args": ["mcp", "serve"]`)
log.Println(` }`)
// Run service
if err := service.Run(); err != nil {
log.Fatal(err)
}
}
+131
View File
@@ -0,0 +1,131 @@
# MCP Hello World Example
The simplest possible MCP-enabled go-micro service.
## What This Shows
- ✅ Automatic documentation extraction from Go comments
- ✅ MCP gateway setup with 3 lines of code
- ✅ Ready for Claude Code integration
- ✅ HTTP endpoint for testing
## Run It
```bash
cd examples/mcp/hello
go run main.go
```
## Test It
### Option 1: HTTP API
```bash
# List available tools
curl http://localhost:3000/mcp/tools | jq
# Call the SayHello tool
curl -X POST http://localhost:3000/mcp/call \
-H "Content-Type: application/json" \
-d '{
"tool": "greeter.Greeter.SayHello",
"input": {"name": "Alice"}
}' | jq
```
### Option 2: Claude Code
In a separate terminal:
```bash
micro mcp serve
```
Add to `~/.claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"greeter": {
"command": "micro",
"args": ["mcp", "serve"]
}
}
}
```
Restart Claude Code and ask:
> "Say hello to Bob using the greeter service"
## How It Works
### 1. Write Normal Go Code
```go
// SayHello greets a person by name. Returns a friendly greeting message.
//
// @example {"name": "Alice"}
func (g *Greeter) SayHello(ctx context.Context, req *HelloRequest, rsp *HelloResponse) error {
rsp.Message = "Hello " + req.Name + "!"
return nil
}
```
### 2. Register the Handler
```go
// Documentation is extracted automatically!
handler := service.Server().NewHandler(new(Greeter))
service.Server().Handle(handler)
```
### 3. Start MCP Gateway
```go
go mcp.ListenAndServe(":3000", mcp.Options{
Registry: service.Options().Registry,
})
```
**That's it!** Your service is now AI-accessible.
## What Gets Extracted
From this code:
```go
// SayHello greets a person by name. Returns a friendly greeting message.
//
// @example {"name": "Alice"}
func (g *Greeter) SayHello(...)
type HelloRequest struct {
Name string `json:"name" description:"Person's name to greet"`
}
```
Claude sees:
```json
{
"name": "greeter.Greeter.SayHello",
"description": "SayHello greets a person by name. Returns a friendly greeting message.",
"inputSchema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Person's name to greet"
}
},
"examples": ["{\"name\": \"Alice\"}"]
}
}
```
## Next Steps
- See `examples/mcp/documented` for a more complete example with multiple endpoints
- Read `/docs/mcp.md` for full documentation
- Check out the [MCP specification](https://modelcontextprotocol.io/)
+65
View File
@@ -0,0 +1,65 @@
// Package main demonstrates a minimal MCP-enabled service.
//
// This is the simplest possible example showing:
// - Automatic documentation extraction from Go comments
// - MCP gateway setup
// - Ready for use with Claude Code
package main
import (
"context"
"log"
"go-micro.dev/v6"
"go-micro.dev/v6/gateway/mcp"
)
// Greeter service handles greeting operations
type Greeter struct{}
// SayHello greets a person by name. Returns a friendly greeting message.
//
// @example {"name": "Alice"}
func (g *Greeter) SayHello(ctx context.Context, req *HelloRequest, rsp *HelloResponse) error {
rsp.Message = "Hello " + req.Name + "!"
return nil
}
// HelloRequest contains the greeting parameters
type HelloRequest struct {
Name string `json:"name" description:"Person's name to greet"`
}
// HelloResponse contains the greeting result
type HelloResponse struct {
Message string `json:"message" description:"The greeting message"`
}
func main() {
// Create service
service := micro.NewService("greeter",
micro.Address(":9090"),
// Start MCP gateway alongside the service
mcp.WithMCP(":3000"),
)
service.Init()
// Register handler — docs extracted automatically from comments
if err := service.Handle(new(Greeter)); err != nil {
log.Fatal(err)
}
log.Println("Greeter service starting...")
log.Println("Service: http://localhost:9090")
log.Println("MCP Gateway: http://localhost:3000")
log.Println("MCP Tools: http://localhost:3000/mcp/tools")
log.Println()
log.Println("Use with Claude Code:")
log.Println(" micro mcp serve")
// Run service
if err := service.Run(); err != nil {
log.Fatal(err)
}
}
+114
View File
@@ -0,0 +1,114 @@
# Platform Example: AI Agents Meet Real Microservices
This example mirrors the [micro/blog](https://github.com/micro/blog) platform — a real microblogging application built on Go Micro. It demonstrates how existing microservices become AI-accessible through MCP with **zero changes to business logic**.
## Services
| Service | Endpoints | Description |
|---------|-----------|-------------|
| **Users** | Signup, Login, GetProfile, UpdateStatus, List | Account management and authentication |
| **Posts** | Create, Read, Update, Delete, List, TagPost, UntagPost, ListTags | Blog posts with markdown and tagging |
| **Comments** | Create, List, Delete | Threaded comments on posts |
| **Mail** | Send, Read | Internal messaging between users |
## Running
```bash
go run .
```
MCP tools available at: http://localhost:3001/mcp/tools
## Agent Scenarios
These are realistic multi-step workflows an AI agent can complete:
### 1. New User Onboarding
```
"Sign up a new user called carol, then write a welcome post introducing herself"
```
The agent will: call Signup → use the returned user ID → call Posts.Create
### 2. Content Creation
```
"Log in as alice and write a blog post about Go concurrency patterns, then tag it with 'golang' and 'concurrency'"
```
The agent will: call Login → call Posts.Create → call TagPost twice
### 3. Social Interaction
```
"List all posts, find the welcome post, and comment on it as bob saying 'Great to be here!'"
```
The agent will: call Posts.List → pick the right post → call Comments.Create
### 4. Cross-Service Workflow
```
"Send a mail from alice to bob welcoming him, then check bob's inbox to confirm delivery"
```
The agent will: call Mail.Send → call Mail.Read to verify
### 5. Platform Overview
```
"Show me all users, all posts, and all tags currently in use"
```
The agent will: call Users.List, Posts.List, and ListTags (potentially in parallel)
## How It Works
The key insight: **you don't need to write any agent-specific code**. The MCP gateway discovers services from the registry, extracts tool schemas from Go types, and generates descriptions from doc comments.
```go
service := micro.NewService("platform",
micro.Address(":9090"),
mcp.WithMCP(":3001"), // This one line makes everything AI-accessible
)
service.Handle(&Users{})
service.Handle(&Posts{})
service.Handle(&Comments{})
service.Handle(&Mail{})
```
Each handler method becomes an MCP tool. The `@example` tags in doc comments give agents sample inputs to learn from.
## Connecting to Claude Code
Add to your Claude Code MCP config:
```json
{
"mcpServers": {
"platform": {
"command": "curl",
"args": ["-s", "http://localhost:3001/mcp/tools"]
}
}
}
```
Or use stdio transport:
```bash
micro mcp serve --registry mdns
```
## Architecture
```
Agent (Claude, GPT, etc.)
MCP Gateway (:3001) ← Discovers services, generates tools
Go Micro RPC (:9090) ← Standard service mesh
├── UserService ← Signup, Login, Profile
├── PostService ← CRUD + Tags
├── CommentService ← Threaded comments
└── MailService ← Internal messaging
```
## Relation to micro/blog
This example is a simplified, self-contained version of [micro/blog](https://github.com/micro/blog). The real platform splits each service into its own binary with protobuf definitions. This example uses Go structs directly for simplicity, but the MCP integration works identically either way — the gateway discovers services from the registry regardless of how they're implemented.
+774
View File
@@ -0,0 +1,774 @@
// Platform example: AI agents interacting with a real microservices platform.
//
// This example mirrors the micro/blog platform (https://github.com/micro/blog)
// — a microblogging platform built on Go Micro with Users, Posts, Comments,
// and Mail services. It demonstrates how existing microservices become
// AI-accessible through MCP with zero changes to business logic.
//
// The services run as a single binary for convenience. In production,
// each would be a separate process discovered via the registry.
//
// Run:
//
// go run .
//
// MCP tools: http://localhost:3001/mcp/tools
//
// Agent scenarios:
//
// "Sign me up as alice with password secret123"
// "Log in as alice and write a blog post about Go concurrency"
// "List all posts and comment on the first one"
// "Send a welcome email to alice"
// "Tag the Go concurrency post with 'golang' and 'tutorial'"
// "Show me alice's profile and all her posts"
package main
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"log"
"sort"
"strings"
"sync"
"time"
"go-micro.dev/v6"
"go-micro.dev/v6/gateway/mcp"
"go-micro.dev/v6/server"
)
// ---------------------------------------------------------------------------
// Users service — account registration, login, profiles
// ---------------------------------------------------------------------------
type User struct {
ID string `json:"id" description:"Unique user identifier"`
Name string `json:"name" description:"Display name"`
Status string `json:"status" description:"Bio or status message"`
CreatedAt int64 `json:"created_at" description:"Unix timestamp of account creation"`
}
type SignupRequest struct {
Name string `json:"name" description:"Username (required, 3-20 characters)"`
Password string `json:"password" description:"Password (required, minimum 6 characters)"`
}
type SignupResponse struct {
User *User `json:"user" description:"The newly created user account"`
Token string `json:"token" description:"Session token for authenticated requests"`
}
type LoginRequest struct {
Name string `json:"name" description:"Username"`
Password string `json:"password" description:"Password"`
}
type LoginResponse struct {
User *User `json:"user" description:"The authenticated user"`
Token string `json:"token" description:"Session token for authenticated requests"`
}
type GetProfileRequest struct {
ID string `json:"id" description:"User ID to look up"`
}
type GetProfileResponse struct {
User *User `json:"user" description:"The user profile"`
}
type UpdateStatusRequest struct {
ID string `json:"id" description:"User ID"`
Status string `json:"status" description:"New bio or status message"`
}
type UpdateStatusResponse struct {
User *User `json:"user" description:"Updated user profile"`
}
type ListUsersRequest struct{}
type ListUsersResponse struct {
Users []*User `json:"users" description:"All registered users"`
}
type Users struct {
mu sync.RWMutex
users map[string]*User
passwords map[string]string // name -> password (plaintext for demo only)
tokens map[string]string // token -> user ID
nextID int
}
func NewUsers() *Users {
return &Users{
users: make(map[string]*User),
passwords: make(map[string]string),
tokens: make(map[string]string),
}
}
// Signup creates a new user account and returns a session token.
// The username must be unique. Use the returned token for authenticated operations.
//
// @example {"name": "alice", "password": "secret123"}
func (s *Users) Signup(ctx context.Context, req *SignupRequest, rsp *SignupResponse) error {
if req.Name == "" || len(req.Name) < 3 {
return fmt.Errorf("name must be at least 3 characters")
}
if req.Password == "" || len(req.Password) < 6 {
return fmt.Errorf("password must be at least 6 characters")
}
s.mu.Lock()
defer s.mu.Unlock()
// Check uniqueness
for _, u := range s.users {
if strings.EqualFold(u.Name, req.Name) {
return fmt.Errorf("username %q is already taken", req.Name)
}
}
s.nextID++
user := &User{
ID: fmt.Sprintf("user-%d", s.nextID),
Name: req.Name,
CreatedAt: time.Now().Unix(),
}
s.users[user.ID] = user
s.passwords[req.Name] = req.Password
token := generateToken()
s.tokens[token] = user.ID
rsp.User = user
rsp.Token = token
return nil
}
// Login authenticates a user and returns a session token.
// Returns an error if the credentials are invalid.
//
// @example {"name": "alice", "password": "secret123"}
func (s *Users) Login(ctx context.Context, req *LoginRequest, rsp *LoginResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
pass, ok := s.passwords[req.Name]
if !ok || pass != req.Password {
return fmt.Errorf("invalid username or password")
}
// Find user by name
for _, u := range s.users {
if u.Name == req.Name {
token := generateToken()
s.tokens[token] = u.ID
rsp.User = u
rsp.Token = token
return nil
}
}
return fmt.Errorf("user not found")
}
// GetProfile retrieves a user's public profile by ID.
//
// @example {"id": "user-1"}
func (s *Users) GetProfile(ctx context.Context, req *GetProfileRequest, rsp *GetProfileResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
u, ok := s.users[req.ID]
if !ok {
return fmt.Errorf("user %s not found", req.ID)
}
rsp.User = u
return nil
}
// UpdateStatus sets a user's bio or status message.
//
// @example {"id": "user-1", "status": "Writing about Go and microservices"}
func (s *Users) UpdateStatus(ctx context.Context, req *UpdateStatusRequest, rsp *UpdateStatusResponse) error {
s.mu.Lock()
defer s.mu.Unlock()
u, ok := s.users[req.ID]
if !ok {
return fmt.Errorf("user %s not found", req.ID)
}
u.Status = req.Status
rsp.User = u
return nil
}
// List returns all registered users on the platform.
//
// @example {}
func (s *Users) List(ctx context.Context, req *ListUsersRequest, rsp *ListUsersResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
for _, u := range s.users {
rsp.Users = append(rsp.Users, u)
}
return nil
}
// ---------------------------------------------------------------------------
// Posts service — blog posts with markdown and tags
// ---------------------------------------------------------------------------
type Post struct {
ID string `json:"id" description:"Unique post identifier"`
Title string `json:"title" description:"Post title"`
Content string `json:"content" description:"Post body in markdown"`
AuthorID string `json:"author_id" description:"ID of the post author"`
AuthorName string `json:"author_name" description:"Display name of the author"`
Tags []string `json:"tags,omitempty" description:"Post tags for categorization"`
CreatedAt int64 `json:"created_at" description:"Unix timestamp of creation"`
UpdatedAt int64 `json:"updated_at" description:"Unix timestamp of last update"`
}
type CreatePostRequest struct {
Title string `json:"title" description:"Post title (required)"`
Content string `json:"content" description:"Post body in markdown (required)"`
AuthorID string `json:"author_id" description:"Author's user ID (required)"`
AuthorName string `json:"author_name" description:"Author's display name (required)"`
}
type CreatePostResponse struct {
Post *Post `json:"post" description:"The newly created post"`
}
type ReadPostRequest struct {
ID string `json:"id" description:"Post ID to retrieve"`
}
type ReadPostResponse struct {
Post *Post `json:"post" description:"The requested post"`
}
type UpdatePostRequest struct {
ID string `json:"id" description:"Post ID to update (required)"`
Title string `json:"title" description:"New title"`
Content string `json:"content" description:"New content in markdown"`
}
type UpdatePostResponse struct {
Post *Post `json:"post" description:"The updated post"`
}
type DeletePostRequest struct {
ID string `json:"id" description:"Post ID to delete"`
}
type DeletePostResponse struct {
Message string `json:"message" description:"Confirmation message"`
}
type ListPostsRequest struct {
AuthorID string `json:"author_id,omitempty" description:"Filter by author ID (optional)"`
}
type ListPostsResponse struct {
Posts []*Post `json:"posts" description:"Posts in reverse chronological order"`
Total int `json:"total" description:"Total number of matching posts"`
}
type TagPostRequest struct {
PostID string `json:"post_id" description:"Post to tag"`
Tag string `json:"tag" description:"Tag to add (lowercase, no spaces)"`
}
type TagPostResponse struct {
Post *Post `json:"post" description:"Post with updated tags"`
}
type UntagPostRequest struct {
PostID string `json:"post_id" description:"Post to untag"`
Tag string `json:"tag" description:"Tag to remove"`
}
type UntagPostResponse struct {
Post *Post `json:"post" description:"Post with updated tags"`
}
type ListTagsRequest struct{}
type ListTagsResponse struct {
Tags []string `json:"tags" description:"All tags in use, sorted alphabetically"`
}
type Posts struct {
mu sync.RWMutex
posts map[string]*Post
nextID int
}
func NewPosts() *Posts {
return &Posts{posts: make(map[string]*Post)}
}
// Create publishes a new blog post. Title, content, author_id, and author_name
// are required. Content supports markdown formatting.
//
// @example {"title": "Getting Started with Go Micro", "content": "Go Micro makes it easy to build microservices...", "author_id": "user-1", "author_name": "alice"}
func (s *Posts) Create(ctx context.Context, req *CreatePostRequest, rsp *CreatePostResponse) error {
if req.Title == "" {
return fmt.Errorf("title is required")
}
if req.Content == "" {
return fmt.Errorf("content is required")
}
if req.AuthorID == "" {
return fmt.Errorf("author_id is required")
}
s.mu.Lock()
defer s.mu.Unlock()
s.nextID++
now := time.Now().Unix()
post := &Post{
ID: fmt.Sprintf("post-%d", s.nextID),
Title: req.Title,
Content: req.Content,
AuthorID: req.AuthorID,
AuthorName: req.AuthorName,
CreatedAt: now,
UpdatedAt: now,
}
s.posts[post.ID] = post
rsp.Post = post
return nil
}
// Read retrieves a single blog post by ID.
//
// @example {"id": "post-1"}
func (s *Posts) Read(ctx context.Context, req *ReadPostRequest, rsp *ReadPostResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
p, ok := s.posts[req.ID]
if !ok {
return fmt.Errorf("post %s not found", req.ID)
}
rsp.Post = p
return nil
}
// Update modifies a blog post's title and/or content.
// Only non-empty fields are updated.
//
// @example {"id": "post-1", "title": "Updated Title", "content": "New content here..."}
func (s *Posts) Update(ctx context.Context, req *UpdatePostRequest, rsp *UpdatePostResponse) error {
s.mu.Lock()
defer s.mu.Unlock()
p, ok := s.posts[req.ID]
if !ok {
return fmt.Errorf("post %s not found", req.ID)
}
if req.Title != "" {
p.Title = req.Title
}
if req.Content != "" {
p.Content = req.Content
}
p.UpdatedAt = time.Now().Unix()
rsp.Post = p
return nil
}
// Delete removes a blog post permanently.
//
// @example {"id": "post-1"}
func (s *Posts) Delete(ctx context.Context, req *DeletePostRequest, rsp *DeletePostResponse) error {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.posts[req.ID]; !ok {
return fmt.Errorf("post %s not found", req.ID)
}
delete(s.posts, req.ID)
rsp.Message = fmt.Sprintf("post %s deleted", req.ID)
return nil
}
// List returns blog posts in reverse chronological order.
// Optionally filter by author_id to see a specific user's posts.
//
// @example {"author_id": "user-1"}
func (s *Posts) List(ctx context.Context, req *ListPostsRequest, rsp *ListPostsResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
for _, p := range s.posts {
if req.AuthorID != "" && p.AuthorID != req.AuthorID {
continue
}
rsp.Posts = append(rsp.Posts, p)
}
sort.Slice(rsp.Posts, func(i, j int) bool {
return rsp.Posts[i].CreatedAt > rsp.Posts[j].CreatedAt
})
rsp.Total = len(rsp.Posts)
return nil
}
// TagPost adds a tag to a post. Tags are useful for categorization
// and discovery. Duplicate tags are ignored.
//
// @example {"post_id": "post-1", "tag": "golang"}
func (s *Posts) TagPost(ctx context.Context, req *TagPostRequest, rsp *TagPostResponse) error {
if req.PostID == "" || req.Tag == "" {
return fmt.Errorf("post_id and tag are required")
}
s.mu.Lock()
defer s.mu.Unlock()
p, ok := s.posts[req.PostID]
if !ok {
return fmt.Errorf("post %s not found", req.PostID)
}
tag := strings.ToLower(strings.TrimSpace(req.Tag))
for _, t := range p.Tags {
if t == tag {
rsp.Post = p
return nil
}
}
p.Tags = append(p.Tags, tag)
p.UpdatedAt = time.Now().Unix()
rsp.Post = p
return nil
}
// UntagPost removes a tag from a post.
//
// @example {"post_id": "post-1", "tag": "golang"}
func (s *Posts) UntagPost(ctx context.Context, req *UntagPostRequest, rsp *UntagPostResponse) error {
s.mu.Lock()
defer s.mu.Unlock()
p, ok := s.posts[req.PostID]
if !ok {
return fmt.Errorf("post %s not found", req.PostID)
}
filtered := make([]string, 0, len(p.Tags))
for _, t := range p.Tags {
if t != req.Tag {
filtered = append(filtered, t)
}
}
p.Tags = filtered
p.UpdatedAt = time.Now().Unix()
rsp.Post = p
return nil
}
// ListTags returns all tags currently in use across all posts.
//
// @example {}
func (s *Posts) ListTags(ctx context.Context, req *ListTagsRequest, rsp *ListTagsResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
seen := make(map[string]bool)
for _, p := range s.posts {
for _, t := range p.Tags {
seen[t] = true
}
}
for t := range seen {
rsp.Tags = append(rsp.Tags, t)
}
sort.Strings(rsp.Tags)
return nil
}
// ---------------------------------------------------------------------------
// Comments service — threaded comments on posts
// ---------------------------------------------------------------------------
type Comment struct {
ID string `json:"id" description:"Unique comment identifier"`
PostID string `json:"post_id" description:"ID of the post this comment belongs to"`
Content string `json:"content" description:"Comment text"`
AuthorID string `json:"author_id" description:"ID of the comment author"`
AuthorName string `json:"author_name" description:"Display name of the author"`
CreatedAt int64 `json:"created_at" description:"Unix timestamp of creation"`
}
type CreateCommentRequest struct {
PostID string `json:"post_id" description:"Post to comment on (required)"`
Content string `json:"content" description:"Comment text (required)"`
AuthorID string `json:"author_id" description:"Author's user ID (required)"`
AuthorName string `json:"author_name" description:"Author's display name (required)"`
}
type CreateCommentResponse struct {
Comment *Comment `json:"comment" description:"The newly created comment"`
}
type ListCommentsRequest struct {
PostID string `json:"post_id,omitempty" description:"Filter by post ID (optional)"`
AuthorID string `json:"author_id,omitempty" description:"Filter by author ID (optional)"`
}
type ListCommentsResponse struct {
Comments []*Comment `json:"comments" description:"Matching comments"`
}
type DeleteCommentRequest struct {
ID string `json:"id" description:"Comment ID to delete"`
}
type DeleteCommentResponse struct {
Message string `json:"message" description:"Confirmation message"`
}
type Comments struct {
mu sync.RWMutex
comments []*Comment
nextID int
}
// Create adds a comment to a blog post. Post ID, content, author_id,
// and author_name are all required.
//
// @example {"post_id": "post-1", "content": "Great article! Very helpful.", "author_id": "user-2", "author_name": "bob"}
func (s *Comments) Create(ctx context.Context, req *CreateCommentRequest, rsp *CreateCommentResponse) error {
if req.PostID == "" {
return fmt.Errorf("post_id is required")
}
if req.Content == "" {
return fmt.Errorf("content is required")
}
if req.AuthorID == "" {
return fmt.Errorf("author_id is required")
}
s.mu.Lock()
defer s.mu.Unlock()
s.nextID++
comment := &Comment{
ID: fmt.Sprintf("comment-%d", s.nextID),
PostID: req.PostID,
Content: req.Content,
AuthorID: req.AuthorID,
AuthorName: req.AuthorName,
CreatedAt: time.Now().Unix(),
}
s.comments = append(s.comments, comment)
rsp.Comment = comment
return nil
}
// List returns comments, optionally filtered by post or author.
// Use post_id to get all comments on a specific post.
//
// @example {"post_id": "post-1"}
func (s *Comments) List(ctx context.Context, req *ListCommentsRequest, rsp *ListCommentsResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
for _, c := range s.comments {
if req.PostID != "" && c.PostID != req.PostID {
continue
}
if req.AuthorID != "" && c.AuthorID != req.AuthorID {
continue
}
rsp.Comments = append(rsp.Comments, c)
}
return nil
}
// Delete removes a comment by ID.
//
// @example {"id": "comment-1"}
func (s *Comments) Delete(ctx context.Context, req *DeleteCommentRequest, rsp *DeleteCommentResponse) error {
s.mu.Lock()
defer s.mu.Unlock()
for i, c := range s.comments {
if c.ID == req.ID {
s.comments = append(s.comments[:i], s.comments[i+1:]...)
rsp.Message = fmt.Sprintf("comment %s deleted", req.ID)
return nil
}
}
return fmt.Errorf("comment %s not found", req.ID)
}
// ---------------------------------------------------------------------------
// Mail service — internal messaging between users
// ---------------------------------------------------------------------------
type MailMessage struct {
ID string `json:"id" description:"Unique message identifier"`
From string `json:"from" description:"Sender username"`
To string `json:"to" description:"Recipient username"`
Subject string `json:"subject" description:"Message subject line"`
Body string `json:"body" description:"Message body text"`
Read bool `json:"read" description:"Whether the message has been read"`
CreatedAt int64 `json:"created_at" description:"Unix timestamp of when the message was sent"`
}
type SendMailRequest struct {
From string `json:"from" description:"Sender username (required)"`
To string `json:"to" description:"Recipient username (required)"`
Subject string `json:"subject" description:"Message subject (required)"`
Body string `json:"body" description:"Message body (required)"`
}
type SendMailResponse struct {
Message *MailMessage `json:"message" description:"The sent message"`
}
type ReadMailRequest struct {
User string `json:"user" description:"Username to read inbox for"`
}
type ReadMailResponse struct {
Messages []*MailMessage `json:"messages" description:"Inbox messages, newest first"`
}
type Mail struct {
mu sync.RWMutex
messages []*MailMessage
nextID int
}
// Send delivers a message to another user on the platform.
// Both sender and recipient are identified by username.
//
// @example {"from": "alice", "to": "bob", "subject": "Welcome!", "body": "Hey Bob, welcome to the platform!"}
func (s *Mail) Send(ctx context.Context, req *SendMailRequest, rsp *SendMailResponse) error {
if req.From == "" || req.To == "" {
return fmt.Errorf("from and to are required")
}
if req.Subject == "" {
return fmt.Errorf("subject is required")
}
s.mu.Lock()
defer s.mu.Unlock()
s.nextID++
msg := &MailMessage{
ID: fmt.Sprintf("mail-%d", s.nextID),
From: req.From,
To: req.To,
Subject: req.Subject,
Body: req.Body,
CreatedAt: time.Now().Unix(),
}
s.messages = append(s.messages, msg)
rsp.Message = msg
return nil
}
// Read returns all messages in a user's inbox, newest first.
//
// @example {"user": "alice"}
func (s *Mail) Read(ctx context.Context, req *ReadMailRequest, rsp *ReadMailResponse) error {
if req.User == "" {
return fmt.Errorf("user is required")
}
s.mu.Lock()
defer s.mu.Unlock()
for i := len(s.messages) - 1; i >= 0; i-- {
if s.messages[i].To == req.User {
s.messages[i].Read = true
rsp.Messages = append(rsp.Messages, s.messages[i])
}
}
return nil
}
// ---------------------------------------------------------------------------
// Main — wire up all services with MCP gateway
// ---------------------------------------------------------------------------
func main() {
service := micro.NewService("platform",
micro.Address(":9090"),
mcp.WithMCP(":3001"),
)
service.Init()
users := NewUsers()
posts := NewPosts()
// Seed some demo data so agents have something to work with
seedData(users, posts)
service.Handle(users)
service.Handle(posts)
service.Handle(&Comments{})
service.Handle(&Mail{},
server.WithEndpointScopes("Mail.Send", "mail:write"),
server.WithEndpointScopes("Mail.Read", "mail:read"),
)
printBanner()
if err := service.Run(); err != nil {
log.Fatal(err)
}
}
func seedData(users *Users, posts *Posts) {
// Create demo users
var aliceRsp SignupResponse
users.Signup(context.Background(), &SignupRequest{
Name: "alice", Password: "secret123",
}, &aliceRsp)
var bobRsp SignupResponse
users.Signup(context.Background(), &SignupRequest{
Name: "bob", Password: "secret123",
}, &bobRsp)
// Alice writes a welcome post
var postRsp CreatePostResponse
posts.Create(context.Background(), &CreatePostRequest{
Title: "Welcome to the Platform",
Content: "This is the first post on our new blogging platform. Built with Go Micro, every service is automatically accessible to AI agents through MCP.",
AuthorID: aliceRsp.User.ID,
AuthorName: "alice",
}, &postRsp)
// Tag it
posts.TagPost(context.Background(), &TagPostRequest{
PostID: postRsp.Post.ID, Tag: "welcome",
}, &TagPostResponse{})
posts.TagPost(context.Background(), &TagPostRequest{
PostID: postRsp.Post.ID, Tag: "go-micro",
}, &TagPostResponse{})
}
func printBanner() {
fmt.Println()
fmt.Println(" Platform Demo — AI-Native Microservices")
fmt.Println()
fmt.Println(" Services: Users, Posts, Comments, Mail")
fmt.Println(" MCP Tools: http://localhost:3001/mcp/tools")
fmt.Println(" RPC: localhost:9090")
fmt.Println()
fmt.Println(" Seeded: alice (user-1), bob (user-2)")
fmt.Println(" 1 post with tags [welcome, go-micro]")
fmt.Println()
fmt.Println(" Try asking an agent:")
fmt.Println()
fmt.Println(` "Sign up a new user called carol"`)
fmt.Println(` "Log in as alice and write a post about Go concurrency patterns"`)
fmt.Println(` "List all posts and comment on the welcome post as bob"`)
fmt.Println(` "Tag alice's post with 'tutorial' and 'golang'"`)
fmt.Println(` "Send a mail from alice to bob welcoming him to the platform"`)
fmt.Println(` "Show me bob's inbox"`)
fmt.Println(` "List all users and show me all tags in use"`)
fmt.Println()
}
func generateToken() string {
b := make([]byte, 16)
rand.Read(b)
return hex.EncodeToString(b)
}
+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/v6"
"go-micro.dev/v6/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.NewService("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)
}
}