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
455 lines
15 KiB
Go
455 lines
15 KiB
Go
// 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))
|
|
}
|