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
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:
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
```
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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/)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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/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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user