You've already forked agentic-coding-workflow
feat(mcp): 16 PM tools + chat tool + 6 resource URIs + admin token UI
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>
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user