Files
wehub-resource-sync e071084ebe
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
chore: import upstream snapshot with attribution
2026-07-13 12:40:33 +08:00

279 lines
7.3 KiB
Go

// 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)
}
}