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 }