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,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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user