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

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