You've already forked agentic-coding-workflow
7d9164d478
Phase F: tools_pm.go — list/get/create/update/close/reopen/assign for projects, workspaces, requirements, issues (16 tools total). Phase G: tools_chat.go — list_recent_messages tool; resources.go — 6 PM resource URI templates (projects, project-detail, requirements, issues, workspaces, workspace-detail). Phase H: frontend admin UI — mcpTokens API client, Pinia store, MCPTokenAdminView with create/revoke/list, router entry, NavBar link. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
225 lines
6.7 KiB
Go
225 lines
6.7 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
|
|
|
|
"github.com/yan1h/agent-coding-workflow/internal/project"
|
|
"github.com/yan1h/agent-coding-workflow/internal/workspace"
|
|
)
|
|
|
|
// registerResources 注册 6 个 PM resource URI template。
|
|
// - pm://projects → static resource, lists all projects
|
|
// - pm://projects/{project_id} → template, single project
|
|
// - pm://projects/{project_id}/requirements → template, list requirements
|
|
// - pm://projects/{project_id}/issues → template, list issues
|
|
// - pm://projects/{project_id}/workspaces → template, list workspaces
|
|
// - pm://workspaces/{workspace_id} → template, single workspace
|
|
func registerResources(srv *mcpsdk.Server, deps ServerDeps) {
|
|
// Static resource: pm://projects
|
|
srv.AddResource(&mcpsdk.Resource{
|
|
URI: "pm://projects",
|
|
Name: "projects",
|
|
Description: "List all accessible projects",
|
|
}, listProjectsResource(deps))
|
|
|
|
// Template resources
|
|
srv.AddResourceTemplate(&mcpsdk.ResourceTemplate{
|
|
URITemplate: "pm://projects/{project_id}",
|
|
Name: "project-detail",
|
|
Description: "Get a single project by ID",
|
|
}, getProjectResource(deps))
|
|
|
|
srv.AddResourceTemplate(&mcpsdk.ResourceTemplate{
|
|
URITemplate: "pm://projects/{project_id}/requirements",
|
|
Name: "project-requirements",
|
|
Description: "List requirements in a project",
|
|
}, listRequirementsResource(deps))
|
|
|
|
srv.AddResourceTemplate(&mcpsdk.ResourceTemplate{
|
|
URITemplate: "pm://projects/{project_id}/issues",
|
|
Name: "project-issues",
|
|
Description: "List issues in a project",
|
|
}, listIssuesResource(deps))
|
|
|
|
srv.AddResourceTemplate(&mcpsdk.ResourceTemplate{
|
|
URITemplate: "pm://projects/{project_id}/workspaces",
|
|
Name: "project-workspaces",
|
|
Description: "List workspaces in a project",
|
|
}, listWorkspacesResource(deps))
|
|
|
|
srv.AddResourceTemplate(&mcpsdk.ResourceTemplate{
|
|
URITemplate: "pm://workspaces/{workspace_id}",
|
|
Name: "workspace-detail",
|
|
Description: "Get a single workspace by ID",
|
|
}, getWorkspaceResource(deps))
|
|
}
|
|
|
|
func listProjectsResource(deps ServerDeps) mcpsdk.ResourceHandler {
|
|
return func(ctx context.Context, req *mcpsdk.ReadResourceRequest) (*mcpsdk.ReadResourceResult, error) {
|
|
caller, err := deps.Caller.Resolve(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
projects, err := deps.Projects.List(ctx, caller, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return marshalResourceResult("pm://projects", projects)
|
|
}
|
|
}
|
|
|
|
func getProjectResource(deps ServerDeps) mcpsdk.ResourceHandler {
|
|
return func(ctx context.Context, req *mcpsdk.ReadResourceRequest) (*mcpsdk.ReadResourceResult, error) {
|
|
caller, err := deps.Caller.Resolve(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pid, err := extractResourceUUID(req.Params.URI, "projects/")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := CheckProjectFromCtx(ctx, pid); err != nil {
|
|
return nil, err
|
|
}
|
|
p, err := deps.Projects.GetByID(ctx, caller, pid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return marshalResourceResult(req.Params.URI, p)
|
|
}
|
|
}
|
|
|
|
func listRequirementsResource(deps ServerDeps) mcpsdk.ResourceHandler {
|
|
return func(ctx context.Context, req *mcpsdk.ReadResourceRequest) (*mcpsdk.ReadResourceResult, error) {
|
|
caller, err := deps.Caller.Resolve(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pid, err := extractResourceUUID(req.Params.URI, "projects/")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := CheckProjectFromCtx(ctx, pid); err != nil {
|
|
return nil, err
|
|
}
|
|
p, err := deps.Projects.GetByID(ctx, caller, pid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
reqs, err := deps.Reqs.List(ctx, caller, p.Slug, project.RequirementFilter{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return marshalResourceResult(req.Params.URI, reqs)
|
|
}
|
|
}
|
|
|
|
func listIssuesResource(deps ServerDeps) mcpsdk.ResourceHandler {
|
|
return func(ctx context.Context, req *mcpsdk.ReadResourceRequest) (*mcpsdk.ReadResourceResult, error) {
|
|
caller, err := deps.Caller.Resolve(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pid, err := extractResourceUUID(req.Params.URI, "projects/")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := CheckProjectFromCtx(ctx, pid); err != nil {
|
|
return nil, err
|
|
}
|
|
p, err := deps.Projects.GetByID(ctx, caller, pid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
issues, err := deps.Issues.List(ctx, caller, p.Slug, project.IssueFilter{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return marshalResourceResult(req.Params.URI, issues)
|
|
}
|
|
}
|
|
|
|
func listWorkspacesResource(deps ServerDeps) mcpsdk.ResourceHandler {
|
|
return func(ctx context.Context, req *mcpsdk.ReadResourceRequest) (*mcpsdk.ReadResourceResult, error) {
|
|
caller, err := deps.Caller.Resolve(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pid, err := extractResourceUUID(req.Params.URI, "projects/")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := CheckProjectFromCtx(ctx, pid); err != nil {
|
|
return nil, err
|
|
}
|
|
p, err := deps.Projects.GetByID(ctx, caller, pid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ws, err := deps.Workspaces.List(ctx, workspaceCallerFrom(caller), p.Slug)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return marshalResourceResult(req.Params.URI, ws)
|
|
}
|
|
}
|
|
|
|
func getWorkspaceResource(deps ServerDeps) mcpsdk.ResourceHandler {
|
|
return func(ctx context.Context, req *mcpsdk.ReadResourceRequest) (*mcpsdk.ReadResourceResult, error) {
|
|
caller, err := deps.Caller.Resolve(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
wid, err := extractResourceUUID(req.Params.URI, "workspaces/")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
w, err := deps.Workspaces.Get(ctx, workspaceCallerFrom(caller), wid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return marshalResourceResult(req.Params.URI, w)
|
|
}
|
|
}
|
|
|
|
// workspaceCallerFrom bridges project.Caller → workspace.Caller.
|
|
func workspaceCallerFrom(c project.Caller) workspace.Caller {
|
|
return workspace.Caller{UserID: c.UserID, IsAdmin: c.IsAdmin}
|
|
}
|
|
|
|
// extractResourceUUID extracts a UUID from a pm:// URI like "pm://projects/{uuid}/requirements".
|
|
func extractResourceUUID(uri, prefix string) (uuid.UUID, error) {
|
|
s := uri
|
|
if strings.HasPrefix(s, "pm://") {
|
|
s = s[len("pm://"):]
|
|
}
|
|
if strings.HasPrefix(s, prefix) {
|
|
s = s[len(prefix):]
|
|
}
|
|
// Trim trailing path segments like "/requirements"
|
|
if idx := strings.Index(s, "/"); idx >= 0 {
|
|
s = s[:idx]
|
|
}
|
|
return uuid.Parse(s)
|
|
}
|
|
|
|
// marshalResourceResult marshals v as JSON text content for resource responses.
|
|
func marshalResourceResult(uri string, v any) (*mcpsdk.ReadResourceResult, error) {
|
|
data, err := json.Marshal(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &mcpsdk.ReadResourceResult{
|
|
Contents: []*mcpsdk.ResourceContents{{
|
|
URI: uri,
|
|
MIMEType: "application/json",
|
|
Text: string(data),
|
|
}},
|
|
}, nil
|
|
}
|