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,63 @@
|
||||
# Agent Demo
|
||||
|
||||
A multi-service project management app that demonstrates AI agents interacting with Go Micro services through MCP.
|
||||
|
||||
## What's Included
|
||||
|
||||
Three services registered in a single process:
|
||||
|
||||
| Service | Endpoints | Description |
|
||||
|---------|-----------|-------------|
|
||||
| **ProjectService** | Create, Get, List | Manage projects with status tracking |
|
||||
| **TaskService** | Create, List, Update | Tasks with assignees, priorities, and status |
|
||||
| **TeamService** | Add, List, Get | Team members with roles and skills |
|
||||
|
||||
The demo starts with seed data: 2 projects, 7 tasks, and 4 team members.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
go run main.go
|
||||
```
|
||||
|
||||
Endpoints:
|
||||
- **MCP Gateway:** http://localhost:3000
|
||||
- **MCP Tools:** http://localhost:3000/mcp/tools
|
||||
- **WebSocket:** ws://localhost:3000/mcp/ws
|
||||
|
||||
## Use with Claude Code
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"demo": {
|
||||
"command": "go",
|
||||
"args": ["run", "main.go"],
|
||||
"cwd": "examples/agent-demo"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Example Prompts
|
||||
|
||||
Try these with Claude Code or any MCP client:
|
||||
|
||||
- "What projects do we have?"
|
||||
- "Show me all tasks assigned to alice"
|
||||
- "Create a high-priority task for bob to review the design mockups"
|
||||
- "Who on the team knows Go?"
|
||||
- "Give me a status update on the Website Redesign project"
|
||||
- "What tasks are still todo on the API v2 migration?"
|
||||
- "Assign the unassigned tasks to charlie"
|
||||
- "Mark task-1 as done"
|
||||
|
||||
## What This Demonstrates
|
||||
|
||||
1. **Zero-config MCP** — Services become AI tools automatically from doc comments
|
||||
2. **Cross-service orchestration** — An agent queries projects, tasks, and team in one conversation
|
||||
3. **Rich tool descriptions** — `description` struct tags and `@example` comments guide the agent
|
||||
4. **Auth scopes** — Read and write operations have separate scopes
|
||||
5. **`WithMCP` one-liner** — MCP gateway starts with a single option
|
||||
|
||||
See the [blog post](/blog/4) for a detailed walkthrough.
|
||||
@@ -0,0 +1,454 @@
|
||||
// Agent Demo — A multi-service project management app
|
||||
//
|
||||
// This example shows three Go Micro services (projects, tasks, team)
|
||||
// working together through the MCP gateway, letting an AI agent
|
||||
// manage projects using natural language.
|
||||
//
|
||||
// Run:
|
||||
//
|
||||
// go run main.go
|
||||
//
|
||||
// Then open the agent playground at http://localhost:8080/agent
|
||||
// or connect Claude Code via: micro mcp serve
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v6"
|
||||
"go-micro.dev/v6/gateway/mcp"
|
||||
"go-micro.dev/v6/server"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Projects service
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type Project struct {
|
||||
ID string `json:"id" description:"Unique project identifier"`
|
||||
Name string `json:"name" description:"Project name"`
|
||||
Description string `json:"description" description:"What the project is about"`
|
||||
Status string `json:"status" description:"Project status: planning, active, or completed"`
|
||||
CreatedAt time.Time `json:"created_at" description:"When the project was created"`
|
||||
}
|
||||
|
||||
type CreateProjectRequest struct {
|
||||
Name string `json:"name" description:"Project name (required)"`
|
||||
Description string `json:"description" description:"Short description of the project"`
|
||||
}
|
||||
|
||||
type CreateProjectResponse struct {
|
||||
Project *Project `json:"project" description:"The newly created project"`
|
||||
}
|
||||
|
||||
type GetProjectRequest struct {
|
||||
ID string `json:"id" description:"Project ID to retrieve"`
|
||||
}
|
||||
|
||||
type GetProjectResponse struct {
|
||||
Project *Project `json:"project" description:"The requested project"`
|
||||
}
|
||||
|
||||
type ListProjectsRequest struct {
|
||||
Status string `json:"status,omitempty" description:"Filter by status: planning, active, completed (optional)"`
|
||||
}
|
||||
|
||||
type ListProjectsResponse struct {
|
||||
Projects []*Project `json:"projects" description:"List of matching projects"`
|
||||
}
|
||||
|
||||
type ProjectService struct {
|
||||
mu sync.RWMutex
|
||||
projects map[string]*Project
|
||||
nextID int
|
||||
}
|
||||
|
||||
// Create creates a new project with the given name and description.
|
||||
// Returns the project with a generated ID and initial status of "planning".
|
||||
//
|
||||
// @example {"name": "Website Redesign", "description": "Redesign the company website with new branding"}
|
||||
func (s *ProjectService) Create(ctx context.Context, req *CreateProjectRequest, rsp *CreateProjectResponse) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.nextID++
|
||||
p := &Project{
|
||||
ID: fmt.Sprintf("proj-%d", s.nextID),
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Status: "planning",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
s.projects[p.ID] = p
|
||||
rsp.Project = p
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get retrieves a project by ID.
|
||||
// Returns an error if the project does not exist.
|
||||
//
|
||||
// @example {"id": "proj-1"}
|
||||
func (s *ProjectService) Get(ctx context.Context, req *GetProjectRequest, rsp *GetProjectResponse) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
p, ok := s.projects[req.ID]
|
||||
if !ok {
|
||||
return fmt.Errorf("project %s not found", req.ID)
|
||||
}
|
||||
rsp.Project = p
|
||||
return nil
|
||||
}
|
||||
|
||||
// List returns all projects, optionally filtered by status.
|
||||
// Valid status values: planning, active, completed.
|
||||
//
|
||||
// @example {"status": "active"}
|
||||
func (s *ProjectService) List(ctx context.Context, req *ListProjectsRequest, rsp *ListProjectsResponse) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
for _, p := range s.projects {
|
||||
if req.Status == "" || p.Status == req.Status {
|
||||
rsp.Projects = append(rsp.Projects, p)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tasks service
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type Task struct {
|
||||
ID string `json:"id" description:"Unique task identifier"`
|
||||
ProjectID string `json:"project_id" description:"ID of the project this task belongs to"`
|
||||
Title string `json:"title" description:"Short task title"`
|
||||
Status string `json:"status" description:"Task status: todo, in_progress, or done"`
|
||||
Assignee string `json:"assignee,omitempty" description:"Username of the person assigned"`
|
||||
Priority string `json:"priority" description:"Priority: low, medium, or high"`
|
||||
}
|
||||
|
||||
type CreateTaskRequest struct {
|
||||
ProjectID string `json:"project_id" description:"Project ID to add the task to (required)"`
|
||||
Title string `json:"title" description:"Task title (required)"`
|
||||
Assignee string `json:"assignee,omitempty" description:"Username to assign (optional)"`
|
||||
Priority string `json:"priority,omitempty" description:"Priority: low, medium, or high (default: medium)"`
|
||||
}
|
||||
|
||||
type CreateTaskResponse struct {
|
||||
Task *Task `json:"task" description:"The newly created task"`
|
||||
}
|
||||
|
||||
type ListTasksRequest struct {
|
||||
ProjectID string `json:"project_id,omitempty" description:"Filter by project ID (optional)"`
|
||||
Assignee string `json:"assignee,omitempty" description:"Filter by assignee username (optional)"`
|
||||
Status string `json:"status,omitempty" description:"Filter by status: todo, in_progress, done (optional)"`
|
||||
}
|
||||
|
||||
type ListTasksResponse struct {
|
||||
Tasks []*Task `json:"tasks" description:"List of matching tasks"`
|
||||
}
|
||||
|
||||
type UpdateTaskRequest struct {
|
||||
ID string `json:"id" description:"Task ID to update"`
|
||||
Status string `json:"status,omitempty" description:"New status: todo, in_progress, or done"`
|
||||
Assignee string `json:"assignee,omitempty" description:"New assignee username"`
|
||||
}
|
||||
|
||||
type UpdateTaskResponse struct {
|
||||
Task *Task `json:"task" description:"The updated task"`
|
||||
}
|
||||
|
||||
type TaskService struct {
|
||||
mu sync.RWMutex
|
||||
tasks map[string]*Task
|
||||
nextID int
|
||||
}
|
||||
|
||||
// Create creates a new task in a project.
|
||||
// Returns the task with a generated ID, initial status of "todo", and default priority of "medium".
|
||||
//
|
||||
// @example {"project_id": "proj-1", "title": "Design homepage mockup", "assignee": "alice", "priority": "high"}
|
||||
func (s *TaskService) Create(ctx context.Context, req *CreateTaskRequest, rsp *CreateTaskResponse) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.nextID++
|
||||
priority := req.Priority
|
||||
if priority == "" {
|
||||
priority = "medium"
|
||||
}
|
||||
t := &Task{
|
||||
ID: fmt.Sprintf("task-%d", s.nextID),
|
||||
ProjectID: req.ProjectID,
|
||||
Title: req.Title,
|
||||
Status: "todo",
|
||||
Assignee: req.Assignee,
|
||||
Priority: priority,
|
||||
}
|
||||
s.tasks[t.ID] = t
|
||||
rsp.Task = t
|
||||
return nil
|
||||
}
|
||||
|
||||
// List returns tasks filtered by project, assignee, or status.
|
||||
// All filters are optional; omit all to list every task.
|
||||
//
|
||||
// @example {"project_id": "proj-1", "status": "todo"}
|
||||
func (s *TaskService) List(ctx context.Context, req *ListTasksRequest, rsp *ListTasksResponse) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
for _, t := range s.tasks {
|
||||
if req.ProjectID != "" && t.ProjectID != req.ProjectID {
|
||||
continue
|
||||
}
|
||||
if req.Assignee != "" && t.Assignee != req.Assignee {
|
||||
continue
|
||||
}
|
||||
if req.Status != "" && t.Status != req.Status {
|
||||
continue
|
||||
}
|
||||
rsp.Tasks = append(rsp.Tasks, t)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update modifies a task's status or assignee.
|
||||
// Only provided fields are changed; omitted fields stay the same.
|
||||
// Returns an error if the task does not exist.
|
||||
//
|
||||
// @example {"id": "task-1", "status": "in_progress"}
|
||||
func (s *TaskService) Update(ctx context.Context, req *UpdateTaskRequest, rsp *UpdateTaskResponse) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
t, ok := s.tasks[req.ID]
|
||||
if !ok {
|
||||
return fmt.Errorf("task %s not found", req.ID)
|
||||
}
|
||||
if req.Status != "" {
|
||||
t.Status = req.Status
|
||||
}
|
||||
if req.Assignee != "" {
|
||||
t.Assignee = req.Assignee
|
||||
}
|
||||
rsp.Task = t
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Team service
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type Member struct {
|
||||
Username string `json:"username" description:"Unique username"`
|
||||
Name string `json:"name" description:"Display name"`
|
||||
Role string `json:"role" description:"Role: engineer, designer, or manager"`
|
||||
Skills []string `json:"skills" description:"List of skills (e.g. go, react, figma)"`
|
||||
}
|
||||
|
||||
type AddMemberRequest struct {
|
||||
Username string `json:"username" description:"Unique username (required)"`
|
||||
Name string `json:"name" description:"Display name (required)"`
|
||||
Role string `json:"role" description:"Role: engineer, designer, or manager"`
|
||||
Skills []string `json:"skills,omitempty" description:"List of skills"`
|
||||
}
|
||||
|
||||
type AddMemberResponse struct {
|
||||
Member *Member `json:"member" description:"The added team member"`
|
||||
}
|
||||
|
||||
type ListMembersRequest struct {
|
||||
Role string `json:"role,omitempty" description:"Filter by role: engineer, designer, manager (optional)"`
|
||||
Skill string `json:"skill,omitempty" description:"Filter by skill (optional, e.g. 'go' or 'react')"`
|
||||
}
|
||||
|
||||
type ListMembersResponse struct {
|
||||
Members []*Member `json:"members" description:"List of matching team members"`
|
||||
}
|
||||
|
||||
type GetMemberRequest struct {
|
||||
Username string `json:"username" description:"Username to look up"`
|
||||
}
|
||||
|
||||
type GetMemberResponse struct {
|
||||
Member *Member `json:"member" description:"The team member"`
|
||||
}
|
||||
|
||||
type TeamService struct {
|
||||
mu sync.RWMutex
|
||||
members map[string]*Member
|
||||
}
|
||||
|
||||
// Add adds a new team member.
|
||||
// Returns the member with their assigned role and skills.
|
||||
//
|
||||
// @example {"username": "alice", "name": "Alice Chen", "role": "engineer", "skills": ["go", "react"]}
|
||||
func (s *TeamService) Add(ctx context.Context, req *AddMemberRequest, rsp *AddMemberResponse) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
m := &Member{
|
||||
Username: req.Username,
|
||||
Name: req.Name,
|
||||
Role: req.Role,
|
||||
Skills: req.Skills,
|
||||
}
|
||||
s.members[m.Username] = m
|
||||
rsp.Member = m
|
||||
return nil
|
||||
}
|
||||
|
||||
// List returns team members, optionally filtered by role or skill.
|
||||
//
|
||||
// @example {"role": "engineer"}
|
||||
func (s *TeamService) List(ctx context.Context, req *ListMembersRequest, rsp *ListMembersResponse) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
for _, m := range s.members {
|
||||
if req.Role != "" && m.Role != req.Role {
|
||||
continue
|
||||
}
|
||||
if req.Skill != "" && !hasSkill(m.Skills, req.Skill) {
|
||||
continue
|
||||
}
|
||||
rsp.Members = append(rsp.Members, m)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get retrieves a team member by username.
|
||||
// Returns an error if the member does not exist.
|
||||
//
|
||||
// @example {"username": "alice"}
|
||||
func (s *TeamService) Get(ctx context.Context, req *GetMemberRequest, rsp *GetMemberResponse) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
m, ok := s.members[req.Username]
|
||||
if !ok {
|
||||
return fmt.Errorf("member %s not found", req.Username)
|
||||
}
|
||||
rsp.Member = m
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasSkill(skills []string, target string) bool {
|
||||
for _, s := range skills {
|
||||
if strings.EqualFold(s, target) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main — wire everything together
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func main() {
|
||||
// Create the service
|
||||
service := micro.NewService("demo",
|
||||
micro.Address(":9090"),
|
||||
// Start MCP gateway alongside the service
|
||||
mcp.WithMCP(":3000"),
|
||||
)
|
||||
service.Init()
|
||||
|
||||
// Register all three handlers with scopes
|
||||
service.Handle(
|
||||
&ProjectService{projects: make(map[string]*Project)},
|
||||
server.WithEndpointScopes("ProjectService.Create", "projects:write"),
|
||||
server.WithEndpointScopes("ProjectService.Get", "projects:read"),
|
||||
server.WithEndpointScopes("ProjectService.List", "projects:read"),
|
||||
)
|
||||
|
||||
service.Handle(
|
||||
&TaskService{tasks: make(map[string]*Task)},
|
||||
server.WithEndpointScopes("TaskService.Create", "tasks:write"),
|
||||
server.WithEndpointScopes("TaskService.List", "tasks:read"),
|
||||
server.WithEndpointScopes("TaskService.Update", "tasks:write"),
|
||||
)
|
||||
|
||||
service.Handle(
|
||||
&TeamService{members: make(map[string]*Member)},
|
||||
server.WithEndpointScopes("TeamService.Add", "team:write"),
|
||||
server.WithEndpointScopes("TeamService.List", "team:read"),
|
||||
server.WithEndpointScopes("TeamService.Get", "team:read"),
|
||||
)
|
||||
|
||||
// Seed some demo data
|
||||
seedData(service.Server())
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println(" Agent Demo")
|
||||
fmt.Println()
|
||||
fmt.Println(" MCP Gateway http://localhost:3000")
|
||||
fmt.Println(" MCP Tools http://localhost:3000/mcp/tools")
|
||||
fmt.Println(" WebSocket ws://localhost:3000/mcp/ws")
|
||||
fmt.Println()
|
||||
fmt.Println(" Try these prompts with Claude Code or the agent playground:")
|
||||
fmt.Println()
|
||||
fmt.Println(" \"What projects do we have?\"")
|
||||
fmt.Println(" \"Create a task for alice to design the new landing page\"")
|
||||
fmt.Println(" \"Show me all high-priority tasks that are still todo\"")
|
||||
fmt.Println(" \"Who on the team knows React?\"")
|
||||
fmt.Println(" \"Give me a status update on the Website Redesign project\"")
|
||||
fmt.Println()
|
||||
|
||||
service.Run()
|
||||
}
|
||||
|
||||
// seedData pre-populates the services with realistic demo data.
|
||||
func seedData(srv server.Server) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Seed team members
|
||||
team := &TeamService{members: make(map[string]*Member)}
|
||||
for _, m := range []AddMemberRequest{
|
||||
{Username: "alice", Name: "Alice Chen", Role: "engineer", Skills: []string{"go", "grpc", "kubernetes"}},
|
||||
{Username: "bob", Name: "Bob Park", Role: "designer", Skills: []string{"figma", "css", "react"}},
|
||||
{Username: "charlie", Name: "Charlie Kim", Role: "engineer", Skills: []string{"go", "react", "postgres"}},
|
||||
{Username: "diana", Name: "Diana Flores", Role: "manager", Skills: []string{"project-management", "scrum"}},
|
||||
} {
|
||||
req := m
|
||||
team.Add(ctx, &req, &AddMemberResponse{})
|
||||
}
|
||||
|
||||
// Seed projects
|
||||
projects := &ProjectService{projects: make(map[string]*Project)}
|
||||
projects.Create(ctx, &CreateProjectRequest{
|
||||
Name: "Website Redesign",
|
||||
Description: "Redesign the company website with new branding and improved UX",
|
||||
}, &CreateProjectResponse{})
|
||||
projects.projects["proj-1"].Status = "active"
|
||||
|
||||
projects.Create(ctx, &CreateProjectRequest{
|
||||
Name: "API v2 Migration",
|
||||
Description: "Migrate all services from REST to gRPC with backward compatibility",
|
||||
}, &CreateProjectResponse{})
|
||||
projects.projects["proj-2"].Status = "planning"
|
||||
|
||||
// Seed tasks
|
||||
tasks := &TaskService{tasks: make(map[string]*Task)}
|
||||
for _, t := range []CreateTaskRequest{
|
||||
{ProjectID: "proj-1", Title: "Design new homepage layout", Assignee: "bob", Priority: "high"},
|
||||
{ProjectID: "proj-1", Title: "Implement responsive nav component", Assignee: "charlie", Priority: "high"},
|
||||
{ProjectID: "proj-1", Title: "Write copy for about page", Priority: "medium"},
|
||||
{ProjectID: "proj-1", Title: "Set up CI/CD for new site", Assignee: "alice", Priority: "medium"},
|
||||
{ProjectID: "proj-2", Title: "Audit existing REST endpoints", Assignee: "alice", Priority: "high"},
|
||||
{ProjectID: "proj-2", Title: "Design gRPC proto files", Priority: "medium"},
|
||||
{ProjectID: "proj-2", Title: "Write migration guide", Assignee: "diana", Priority: "low"},
|
||||
} {
|
||||
req := t
|
||||
tasks.Create(ctx, &req, &CreateTaskResponse{})
|
||||
}
|
||||
// Mark a couple tasks as in_progress
|
||||
tasks.tasks["task-1"].Status = "in_progress"
|
||||
tasks.tasks["task-5"].Status = "in_progress"
|
||||
|
||||
// Register the seeded handlers (replace the empty ones registered above)
|
||||
// Note: in a real app these would be separate services. Here we register
|
||||
// pre-seeded instances so the demo starts with data.
|
||||
srv.Handle(srv.NewHandler(projects))
|
||||
srv.Handle(srv.NewHandler(tasks))
|
||||
srv.Handle(srv.NewHandler(team))
|
||||
}
|
||||
Reference in New Issue
Block a user