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
279 lines
7.3 KiB
Go
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)
|
|
}
|
|
}
|