From da30f3144aa9d7ebcad3ce3b76183147af89573e Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 17:11:16 +0000 Subject: [PATCH] refactor(mcp): record subsystem tools centrally Co-Authored-By: Virgil --- pkg/mcp/agentic/dispatch.go | 5 +++-- pkg/mcp/agentic/epic.go | 32 +++++++++++++++++--------------- pkg/mcp/agentic/issue.go | 7 ++++--- pkg/mcp/agentic/mirror.go | 6 ++++-- pkg/mcp/agentic/plan.go | 18 ++++++++++-------- pkg/mcp/agentic/pr.go | 23 +++++++++++++---------- pkg/mcp/agentic/prep.go | 29 +++++++++++++++-------------- pkg/mcp/agentic/resume.go | 5 +++-- pkg/mcp/agentic/review_queue.go | 6 ++++-- pkg/mcp/agentic/status.go | 5 +++-- pkg/mcp/agentic/watch.go | 6 ++++-- pkg/mcp/brain/brain.go | 5 ++--- pkg/mcp/brain/direct.go | 11 ++++++----- pkg/mcp/brain/tools.go | 11 ++++++----- pkg/mcp/bridge_test.go | 16 +++++++++++++++- pkg/mcp/ide/ide.go | 9 ++++----- pkg/mcp/ide/tools_build.go | 10 ++++++---- pkg/mcp/ide/tools_chat.go | 14 ++++++++------ pkg/mcp/ide/tools_dashboard.go | 10 ++++++---- pkg/mcp/mcp.go | 2 +- pkg/mcp/registry.go | 14 ++++++++++++-- pkg/mcp/subsystem.go | 6 ++---- pkg/mcp/subsystem_test.go | 6 ++---- 23 files changed, 150 insertions(+), 106 deletions(-) diff --git a/pkg/mcp/agentic/dispatch.go b/pkg/mcp/agentic/dispatch.go index bbe277b..0f31114 100644 --- a/pkg/mcp/agentic/dispatch.go +++ b/pkg/mcp/agentic/dispatch.go @@ -43,8 +43,9 @@ type DispatchOutput struct { OutputFile string `json:"output_file,omitempty"` } -func (s *PrepSubsystem) registerDispatchTool(server *mcp.Server) { - mcp.AddTool(server, &mcp.Tool{ +func (s *PrepSubsystem) registerDispatchTool(svc *coremcp.Service) { + server := svc.Server() + coremcp.AddToolRecorded(svc, server, "agentic", &mcp.Tool{ Name: "agentic_dispatch", Description: "Dispatch a subagent (Gemini, Codex, or Claude) to work on a task. Preps a sandboxed workspace first, then spawns the agent inside it. Templates: conventions, security, coding.", }, s.dispatch) diff --git a/pkg/mcp/agentic/epic.go b/pkg/mcp/agentic/epic.go index 2cf83e0..a119808 100644 --- a/pkg/mcp/agentic/epic.go +++ b/pkg/mcp/agentic/epic.go @@ -10,6 +10,7 @@ import ( "net/http" "strings" + coremcp "dappco.re/go/mcp/pkg/mcp" coreerr "forge.lthn.ai/core/go-log" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -19,23 +20,23 @@ import ( // EpicInput is the input for agentic_create_epic. type EpicInput struct { Repo string `json:"repo"` // Target repo (e.g. "go-scm") - Org string `json:"org,omitempty"` // Forge org (default "core") - Title string `json:"title"` // Epic title - Body string `json:"body,omitempty"` // Epic description (above checklist) - Tasks []string `json:"tasks"` // Sub-task titles (become child issues) - Labels []string `json:"labels,omitempty"` // Labels for epic + children (e.g. ["agentic"]) - Dispatch bool `json:"dispatch,omitempty"` // Auto-dispatch agents to each child - Agent string `json:"agent,omitempty"` // Agent type for dispatch (default "claude") - Template string `json:"template,omitempty"` // Prompt template for dispatch (default "coding") + Org string `json:"org,omitempty"` // Forge org (default "core") + Title string `json:"title"` // Epic title + Body string `json:"body,omitempty"` // Epic description (above checklist) + Tasks []string `json:"tasks"` // Sub-task titles (become child issues) + Labels []string `json:"labels,omitempty"` // Labels for epic + children (e.g. ["agentic"]) + Dispatch bool `json:"dispatch,omitempty"` // Auto-dispatch agents to each child + Agent string `json:"agent,omitempty"` // Agent type for dispatch (default "claude") + Template string `json:"template,omitempty"` // Prompt template for dispatch (default "coding") } // EpicOutput is the output for agentic_create_epic. type EpicOutput struct { - Success bool `json:"success"` - EpicNumber int `json:"epic_number"` - EpicURL string `json:"epic_url"` - Children []ChildRef `json:"children"` - Dispatched int `json:"dispatched,omitempty"` + Success bool `json:"success"` + EpicNumber int `json:"epic_number"` + EpicURL string `json:"epic_url"` + Children []ChildRef `json:"children"` + Dispatched int `json:"dispatched,omitempty"` } // ChildRef references a child issue. @@ -45,8 +46,9 @@ type ChildRef struct { URL string `json:"url"` } -func (s *PrepSubsystem) registerEpicTool(server *mcp.Server) { - mcp.AddTool(server, &mcp.Tool{ +func (s *PrepSubsystem) registerEpicTool(svc *coremcp.Service) { + server := svc.Server() + coremcp.AddToolRecorded(svc, server, "agentic", &mcp.Tool{ Name: "agentic_create_epic", Description: "Create an epic issue with child issues on Forge. Each task becomes a child issue linked via checklist. Optionally auto-dispatch agents to work each child.", }, s.createEpic) diff --git a/pkg/mcp/agentic/issue.go b/pkg/mcp/agentic/issue.go index 05c6392..06e3c40 100644 --- a/pkg/mcp/agentic/issue.go +++ b/pkg/mcp/agentic/issue.go @@ -32,13 +32,14 @@ type forgeIssue struct { } `json:"assignee"` } -func (s *PrepSubsystem) registerIssueTools(server *mcp.Server) { - mcp.AddTool(server, &mcp.Tool{ +func (s *PrepSubsystem) registerIssueTools(svc *coremcp.Service) { + server := svc.Server() + coremcp.AddToolRecorded(svc, server, "agentic", &mcp.Tool{ Name: "agentic_dispatch_issue", Description: "Dispatch an agent to work on a Forge issue. Assigns the issue as a lock, prepends the issue body to TODO.md, creates an issue-specific branch, and spawns the agent.", }, s.dispatchIssue) - mcp.AddTool(server, &mcp.Tool{ + coremcp.AddToolRecorded(svc, server, "agentic", &mcp.Tool{ Name: "agentic_pr", Description: "Create a pull request from an agent workspace. Pushes the branch and creates a Forge PR linked to the tracked issue, if any.", }, s.createPR) diff --git a/pkg/mcp/agentic/mirror.go b/pkg/mcp/agentic/mirror.go index 2318c07..9cfad5f 100644 --- a/pkg/mcp/agentic/mirror.go +++ b/pkg/mcp/agentic/mirror.go @@ -8,6 +8,7 @@ import ( "os/exec" "path/filepath" + coremcp "dappco.re/go/mcp/pkg/mcp" coreerr "forge.lthn.ai/core/go-log" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -37,8 +38,9 @@ type MirrorSync struct { Skipped string `json:"skipped,omitempty"` } -func (s *PrepSubsystem) registerMirrorTool(server *mcp.Server) { - mcp.AddTool(server, &mcp.Tool{ +func (s *PrepSubsystem) registerMirrorTool(svc *coremcp.Service) { + server := svc.Server() + coremcp.AddToolRecorded(svc, server, "agentic", &mcp.Tool{ Name: "agentic_mirror", Description: "Mirror Forge repositories to GitHub and open a GitHub PR when there are commits ahead of the remote mirror.", }, s.mirror) diff --git a/pkg/mcp/agentic/plan.go b/pkg/mcp/agentic/plan.go index 5635b6b..318c0a2 100644 --- a/pkg/mcp/agentic/plan.go +++ b/pkg/mcp/agentic/plan.go @@ -11,6 +11,7 @@ import ( "strings" "time" + coremcp "dappco.re/go/mcp/pkg/mcp" coreio "forge.lthn.ai/core/go-io" coreerr "forge.lthn.ai/core/go-log" "github.com/modelcontextprotocol/go-sdk/mcp" @@ -169,39 +170,40 @@ type PlanCheckpointOutput struct { // --- Registration --- -func (s *PrepSubsystem) registerPlanTools(server *mcp.Server) { - mcp.AddTool(server, &mcp.Tool{ +func (s *PrepSubsystem) registerPlanTools(svc *coremcp.Service) { + server := svc.Server() + coremcp.AddToolRecorded(svc, server, "agentic", &mcp.Tool{ Name: "agentic_plan_create", Description: "Create an implementation plan. Plans track phased work with acceptance criteria, status lifecycle (draft → ready → in_progress → needs_verification → verified → approved), and per-phase progress.", }, s.planCreate) - mcp.AddTool(server, &mcp.Tool{ + coremcp.AddToolRecorded(svc, server, "agentic", &mcp.Tool{ Name: "agentic_plan_read", Description: "Read an implementation plan by ID. Returns the full plan with all phases, criteria, and status.", }, s.planRead) // agentic_plan_status is kept as a user-facing alias for the read tool. - mcp.AddTool(server, &mcp.Tool{ + coremcp.AddToolRecorded(svc, server, "agentic", &mcp.Tool{ Name: "agentic_plan_status", Description: "Get the current status of an implementation plan by ID. Returns the full plan with all phases, criteria, and status.", }, s.planRead) - mcp.AddTool(server, &mcp.Tool{ + coremcp.AddToolRecorded(svc, server, "agentic", &mcp.Tool{ Name: "agentic_plan_update", Description: "Update an implementation plan. Supports partial updates — only provided fields are changed. Use this to advance status, update phases, or add notes.", }, s.planUpdate) - mcp.AddTool(server, &mcp.Tool{ + coremcp.AddToolRecorded(svc, server, "agentic", &mcp.Tool{ Name: "agentic_plan_delete", Description: "Delete an implementation plan by ID. Permanently removes the plan file.", }, s.planDelete) - mcp.AddTool(server, &mcp.Tool{ + coremcp.AddToolRecorded(svc, server, "agentic", &mcp.Tool{ Name: "agentic_plan_list", Description: "List implementation plans. Supports filtering by status (draft, ready, in_progress, etc.) and repo.", }, s.planList) - mcp.AddTool(server, &mcp.Tool{ + coremcp.AddToolRecorded(svc, server, "agentic", &mcp.Tool{ Name: "agentic_plan_checkpoint", Description: "Record a checkpoint for a plan phase and optionally mark the phase done.", }, s.planCheckpoint) diff --git a/pkg/mcp/agentic/pr.go b/pkg/mcp/agentic/pr.go index 1622dc0..e4f89f4 100644 --- a/pkg/mcp/agentic/pr.go +++ b/pkg/mcp/agentic/pr.go @@ -12,6 +12,7 @@ import ( "path/filepath" "strings" + coremcp "dappco.re/go/mcp/pkg/mcp" coreio "forge.lthn.ai/core/go-io" coreerr "forge.lthn.ai/core/go-log" "github.com/modelcontextprotocol/go-sdk/mcp" @@ -21,11 +22,11 @@ import ( // CreatePRInput is the input for agentic_create_pr. type CreatePRInput struct { - Workspace string `json:"workspace"` // workspace name (e.g. "mcp-1773581873") - Title string `json:"title,omitempty"` // PR title (default: task description) - Body string `json:"body,omitempty"` // PR body (default: auto-generated) - Base string `json:"base,omitempty"` // base branch (default: "main") - DryRun bool `json:"dry_run,omitempty"` // preview without creating + Workspace string `json:"workspace"` // workspace name (e.g. "mcp-1773581873") + Title string `json:"title,omitempty"` // PR title (default: task description) + Body string `json:"body,omitempty"` // PR body (default: auto-generated) + Base string `json:"base,omitempty"` // base branch (default: "main") + DryRun bool `json:"dry_run,omitempty"` // preview without creating } // CreatePROutput is the output for agentic_create_pr. @@ -39,8 +40,9 @@ type CreatePROutput struct { Pushed bool `json:"pushed"` } -func (s *PrepSubsystem) registerCreatePRTool(server *mcp.Server) { - mcp.AddTool(server, &mcp.Tool{ +func (s *PrepSubsystem) registerCreatePRTool(svc *coremcp.Service) { + server := svc.Server() + coremcp.AddToolRecorded(svc, server, "agentic", &mcp.Tool{ Name: "agentic_create_pr", Description: "Create a pull request from an agent workspace. Pushes the branch to Forge and opens a PR. Links to the source issue if one was tracked.", }, s.createPR) @@ -217,7 +219,7 @@ func (s *PrepSubsystem) commentOnIssue(ctx context.Context, org, repo string, is // ListPRsInput is the input for agentic_list_prs. type ListPRsInput struct { Org string `json:"org,omitempty"` // forge org (default "core") - Repo string `json:"repo,omitempty"` // specific repo, or empty for all + Repo string `json:"repo,omitempty"` // specific repo, or empty for all State string `json:"state,omitempty"` // "open" (default), "closed", "all" Limit int `json:"limit,omitempty"` // max results (default 20) } @@ -243,8 +245,9 @@ type PRInfo struct { URL string `json:"url"` } -func (s *PrepSubsystem) registerListPRsTool(server *mcp.Server) { - mcp.AddTool(server, &mcp.Tool{ +func (s *PrepSubsystem) registerListPRsTool(svc *coremcp.Service) { + server := svc.Server() + coremcp.AddToolRecorded(svc, server, "agentic", &mcp.Tool{ Name: "agentic_list_prs", Description: "List pull requests across Forge repos. Filter by org, repo, and state (open/closed/all).", }, s.listPRs) diff --git a/pkg/mcp/agentic/prep.go b/pkg/mcp/agentic/prep.go index 175fdd2..55aae8d 100644 --- a/pkg/mcp/agentic/prep.go +++ b/pkg/mcp/agentic/prep.go @@ -124,29 +124,30 @@ func sanitizeRepoPathSegment(value, field string, allowSubdirs bool) (string, er func (s *PrepSubsystem) Name() string { return "agentic" } // RegisterTools implements mcp.Subsystem. -func (s *PrepSubsystem) RegisterTools(server *mcp.Server) { - mcp.AddTool(server, &mcp.Tool{ +func (s *PrepSubsystem) RegisterTools(svc *coremcp.Service) { + server := svc.Server() + coremcp.AddToolRecorded(svc, server, "agentic", &mcp.Tool{ Name: "agentic_prep_workspace", Description: "Prepare a sandboxed agent workspace with TODO.md, CLAUDE.md, CONTEXT.md, CONSUMERS.md, RECENT.md, and a git clone of the target repo in src/.", }, s.prepWorkspace) - s.registerDispatchTool(server) - s.registerIssueTools(server) - s.registerStatusTool(server) - s.registerResumeTool(server) - s.registerCreatePRTool(server) - s.registerListPRsTool(server) - s.registerEpicTool(server) - s.registerWatchTool(server) - s.registerReviewQueueTool(server) - s.registerMirrorTool(server) + s.registerDispatchTool(svc) + s.registerIssueTools(svc) + s.registerStatusTool(svc) + s.registerResumeTool(svc) + s.registerCreatePRTool(svc) + s.registerListPRsTool(svc) + s.registerEpicTool(svc) + s.registerWatchTool(svc) + s.registerReviewQueueTool(svc) + s.registerMirrorTool(svc) - mcp.AddTool(server, &mcp.Tool{ + coremcp.AddToolRecorded(svc, server, "agentic", &mcp.Tool{ Name: "agentic_scan", Description: "Scan Forge repos for open issues with actionable labels (agentic, help-wanted, bug).", }, s.scan) - s.registerPlanTools(server) + s.registerPlanTools(svc) } // Shutdown implements mcp.SubsystemWithShutdown. diff --git a/pkg/mcp/agentic/resume.go b/pkg/mcp/agentic/resume.go index 5f7a463..c47c539 100644 --- a/pkg/mcp/agentic/resume.go +++ b/pkg/mcp/agentic/resume.go @@ -39,8 +39,9 @@ type ResumeOutput struct { Prompt string `json:"prompt,omitempty"` } -func (s *PrepSubsystem) registerResumeTool(server *mcp.Server) { - mcp.AddTool(server, &mcp.Tool{ +func (s *PrepSubsystem) registerResumeTool(svc *coremcp.Service) { + server := svc.Server() + coremcp.AddToolRecorded(svc, server, "agentic", &mcp.Tool{ Name: "agentic_resume", Description: "Resume a blocked agent workspace. Writes ANSWER.md if an answer is provided, then relaunches the agent with instructions to read it and continue.", }, s.resume) diff --git a/pkg/mcp/agentic/review_queue.go b/pkg/mcp/agentic/review_queue.go index 292501e..f0fc81d 100644 --- a/pkg/mcp/agentic/review_queue.go +++ b/pkg/mcp/agentic/review_queue.go @@ -13,6 +13,7 @@ import ( "strings" "time" + coremcp "dappco.re/go/mcp/pkg/mcp" coreio "forge.lthn.ai/core/go-io" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -57,8 +58,9 @@ func reviewQueueHomeDir() string { return home } -func (s *PrepSubsystem) registerReviewQueueTool(server *mcp.Server) { - mcp.AddTool(server, &mcp.Tool{ +func (s *PrepSubsystem) registerReviewQueueTool(svc *coremcp.Service) { + server := svc.Server() + coremcp.AddToolRecorded(svc, server, "agentic", &mcp.Tool{ Name: "agentic_review_queue", Description: "Process repositories that are ahead of the GitHub mirror and summarise review findings.", }, s.reviewQueue) diff --git a/pkg/mcp/agentic/status.go b/pkg/mcp/agentic/status.go index 7cba615..78ffd58 100644 --- a/pkg/mcp/agentic/status.go +++ b/pkg/mcp/agentic/status.go @@ -105,8 +105,9 @@ type WorkspaceInfo struct { Runs int `json:"runs"` } -func (s *PrepSubsystem) registerStatusTool(server *mcp.Server) { - mcp.AddTool(server, &mcp.Tool{ +func (s *PrepSubsystem) registerStatusTool(svc *coremcp.Service) { + server := svc.Server() + coremcp.AddToolRecorded(svc, server, "agentic", &mcp.Tool{ Name: "agentic_status", Description: "List agent workspaces and their status (running, completed, blocked, failed). Shows blocked agents with their questions.", }, s.status) diff --git a/pkg/mcp/agentic/watch.go b/pkg/mcp/agentic/watch.go index bc0b0f1..3d90d75 100644 --- a/pkg/mcp/agentic/watch.go +++ b/pkg/mcp/agentic/watch.go @@ -7,6 +7,7 @@ import ( "path/filepath" "time" + coremcp "dappco.re/go/mcp/pkg/mcp" coreerr "forge.lthn.ai/core/go-log" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -37,8 +38,9 @@ type WatchResult struct { PRURL string `json:"pr_url,omitempty"` } -func (s *PrepSubsystem) registerWatchTool(server *mcp.Server) { - mcp.AddTool(server, &mcp.Tool{ +func (s *PrepSubsystem) registerWatchTool(svc *coremcp.Service) { + server := svc.Server() + coremcp.AddToolRecorded(svc, server, "agentic", &mcp.Tool{ Name: "agentic_watch", Description: "Watch running or queued agent workspaces until they finish and return a completion summary.", }, s.watch) diff --git a/pkg/mcp/brain/brain.go b/pkg/mcp/brain/brain.go index 471fa85..d8b1e3f 100644 --- a/pkg/mcp/brain/brain.go +++ b/pkg/mcp/brain/brain.go @@ -10,7 +10,6 @@ import ( coremcp "dappco.re/go/mcp/pkg/mcp" "dappco.re/go/mcp/pkg/mcp/ide" coreerr "forge.lthn.ai/core/go-log" - "github.com/modelcontextprotocol/go-sdk/mcp" ) // errBridgeNotAvailable is returned when a tool requires the Laravel bridge @@ -48,8 +47,8 @@ func (s *Subsystem) SetNotifier(n coremcp.Notifier) { } // RegisterTools implements mcp.Subsystem. -func (s *Subsystem) RegisterTools(server *mcp.Server) { - s.registerBrainTools(server) +func (s *Subsystem) RegisterTools(svc *coremcp.Service) { + s.registerBrainTools(svc) } func (s *Subsystem) handleBridgeMessage(msg ide.BridgeMessage) { diff --git a/pkg/mcp/brain/direct.go b/pkg/mcp/brain/direct.go index 463d9c2..739b9a6 100644 --- a/pkg/mcp/brain/direct.go +++ b/pkg/mcp/brain/direct.go @@ -75,23 +75,24 @@ func NewDirect() *DirectSubsystem { func (s *DirectSubsystem) Name() string { return "brain" } // RegisterTools implements mcp.Subsystem. -func (s *DirectSubsystem) RegisterTools(server *mcp.Server) { - mcp.AddTool(server, &mcp.Tool{ +func (s *DirectSubsystem) RegisterTools(svc *coremcp.Service) { + server := svc.Server() + coremcp.AddToolRecorded(svc, server, "brain", &mcp.Tool{ Name: "brain_remember", Description: "Store a memory in OpenBrain. Types: fact, decision, observation, plan, convention, architecture, research, documentation, service, bug, pattern, context, procedure.", }, s.remember) - mcp.AddTool(server, &mcp.Tool{ + coremcp.AddToolRecorded(svc, server, "brain", &mcp.Tool{ Name: "brain_recall", Description: "Semantic search across OpenBrain memories. Returns memories ranked by similarity. Use agent_id 'cladius' for Cladius's memories.", }, s.recall) - mcp.AddTool(server, &mcp.Tool{ + coremcp.AddToolRecorded(svc, server, "brain", &mcp.Tool{ Name: "brain_forget", Description: "Remove a memory from OpenBrain by ID.", }, s.forget) - mcp.AddTool(server, &mcp.Tool{ + coremcp.AddToolRecorded(svc, server, "brain", &mcp.Tool{ Name: "brain_list", Description: "List memories in OpenBrain with optional filtering by project, type, and agent.", }, s.list) diff --git a/pkg/mcp/brain/tools.go b/pkg/mcp/brain/tools.go index 4a9ca81..ec8cd43 100644 --- a/pkg/mcp/brain/tools.go +++ b/pkg/mcp/brain/tools.go @@ -126,23 +126,24 @@ type ListOutput struct { // -- Tool registration -------------------------------------------------------- -func (s *Subsystem) registerBrainTools(server *mcp.Server) { - mcp.AddTool(server, &mcp.Tool{ +func (s *Subsystem) registerBrainTools(svc *coremcp.Service) { + server := svc.Server() + coremcp.AddToolRecorded(svc, server, "brain", &mcp.Tool{ Name: "brain_remember", Description: "Store a memory in the shared OpenBrain knowledge store. Persists decisions, observations, conventions, research, plans, bugs, or architecture knowledge for other agents.", }, s.brainRemember) - mcp.AddTool(server, &mcp.Tool{ + coremcp.AddToolRecorded(svc, server, "brain", &mcp.Tool{ Name: "brain_recall", Description: "Semantic search across the shared OpenBrain knowledge store. Returns memories ranked by similarity to your query, with optional filtering.", }, s.brainRecall) - mcp.AddTool(server, &mcp.Tool{ + coremcp.AddToolRecorded(svc, server, "brain", &mcp.Tool{ Name: "brain_forget", Description: "Remove a memory from the shared OpenBrain knowledge store. Permanently deletes from both database and vector index.", }, s.brainForget) - mcp.AddTool(server, &mcp.Tool{ + coremcp.AddToolRecorded(svc, server, "brain", &mcp.Tool{ Name: "brain_list", Description: "List memories in the shared OpenBrain knowledge store. Supports filtering by project, type, and agent. No vector search -- use brain_recall for semantic queries.", }, s.brainList) diff --git a/pkg/mcp/bridge_test.go b/pkg/mcp/bridge_test.go index 8f59052..c8a869c 100644 --- a/pkg/mcp/bridge_test.go +++ b/pkg/mcp/bridge_test.go @@ -13,6 +13,8 @@ import ( "github.com/gin-gonic/gin" + "dappco.re/go/mcp/pkg/mcp/agentic" + "dappco.re/go/mcp/pkg/mcp/brain" api "forge.lthn.ai/core/api" ) @@ -21,7 +23,13 @@ func init() { } func TestBridgeToAPI_Good_AllTools(t *testing.T) { - svc, err := New(Options{WorkspaceRoot: t.TempDir()}) + svc, err := New(Options{ + WorkspaceRoot: t.TempDir(), + Subsystems: []Subsystem{ + brain.New(nil), + agentic.NewPrep(), + }, + }) if err != nil { t.Fatal(err) } @@ -49,6 +57,12 @@ func TestBridgeToAPI_Good_AllTools(t *testing.T) { t.Errorf("bridge has tool %q not found in service", td.Name) } } + + for _, want := range []string{"brain_list", "agentic_plan_create", "ide_dashboard_overview"} { + if !svcNames[want] { + t.Fatalf("expected recorded tool %q to be present", want) + } + } } func TestBridgeToAPI_Good_DescribableGroup(t *testing.T) { diff --git a/pkg/mcp/ide/ide.go b/pkg/mcp/ide/ide.go index f485708..3713d4f 100644 --- a/pkg/mcp/ide/ide.go +++ b/pkg/mcp/ide/ide.go @@ -12,7 +12,6 @@ import ( coremcp "dappco.re/go/mcp/pkg/mcp" coreerr "forge.lthn.ai/core/go-log" "forge.lthn.ai/core/go-ws" - "github.com/modelcontextprotocol/go-sdk/mcp" ) // errBridgeNotAvailable is returned when a tool requires the Laravel bridge @@ -67,10 +66,10 @@ func New(hub *ws.Hub, cfg Config) *Subsystem { func (s *Subsystem) Name() string { return "ide" } // RegisterTools implements mcp.Subsystem. -func (s *Subsystem) RegisterTools(server *mcp.Server) { - s.registerChatTools(server) - s.registerBuildTools(server) - s.registerDashboardTools(server) +func (s *Subsystem) RegisterTools(svc *coremcp.Service) { + s.registerChatTools(svc) + s.registerBuildTools(svc) + s.registerDashboardTools(svc) } // Shutdown implements mcp.SubsystemWithShutdown. diff --git a/pkg/mcp/ide/tools_build.go b/pkg/mcp/ide/tools_build.go index a8d337e..e5af0ec 100644 --- a/pkg/mcp/ide/tools_build.go +++ b/pkg/mcp/ide/tools_build.go @@ -6,6 +6,7 @@ import ( "context" "time" + coremcp "dappco.re/go/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -68,18 +69,19 @@ type BuildLogsOutput struct { Lines []string `json:"lines"` } -func (s *Subsystem) registerBuildTools(server *mcp.Server) { - mcp.AddTool(server, &mcp.Tool{ +func (s *Subsystem) registerBuildTools(svc *coremcp.Service) { + server := svc.Server() + coremcp.AddToolRecorded(svc, server, "ide", &mcp.Tool{ Name: "ide_build_status", Description: "Get the status of a specific build", }, s.buildStatus) - mcp.AddTool(server, &mcp.Tool{ + coremcp.AddToolRecorded(svc, server, "ide", &mcp.Tool{ Name: "ide_build_list", Description: "List recent builds, optionally filtered by repository", }, s.buildList) - mcp.AddTool(server, &mcp.Tool{ + coremcp.AddToolRecorded(svc, server, "ide", &mcp.Tool{ Name: "ide_build_logs", Description: "Retrieve log output for a build", }, s.buildLogs) diff --git a/pkg/mcp/ide/tools_chat.go b/pkg/mcp/ide/tools_chat.go index 1bed0e0..576d2ba 100644 --- a/pkg/mcp/ide/tools_chat.go +++ b/pkg/mcp/ide/tools_chat.go @@ -6,6 +6,7 @@ import ( "context" "time" + coremcp "dappco.re/go/mcp/pkg/mcp" coreerr "forge.lthn.ai/core/go-log" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -114,28 +115,29 @@ type PlanStatusOutput struct { Steps []PlanStep `json:"steps"` } -func (s *Subsystem) registerChatTools(server *mcp.Server) { - mcp.AddTool(server, &mcp.Tool{ +func (s *Subsystem) registerChatTools(svc *coremcp.Service) { + server := svc.Server() + coremcp.AddToolRecorded(svc, server, "ide", &mcp.Tool{ Name: "ide_chat_send", Description: "Send a message to an agent chat session", }, s.chatSend) - mcp.AddTool(server, &mcp.Tool{ + coremcp.AddToolRecorded(svc, server, "ide", &mcp.Tool{ Name: "ide_chat_history", Description: "Retrieve message history for a chat session", }, s.chatHistory) - mcp.AddTool(server, &mcp.Tool{ + coremcp.AddToolRecorded(svc, server, "ide", &mcp.Tool{ Name: "ide_session_list", Description: "List active agent sessions", }, s.sessionList) - mcp.AddTool(server, &mcp.Tool{ + coremcp.AddToolRecorded(svc, server, "ide", &mcp.Tool{ Name: "ide_session_create", Description: "Create a new agent session", }, s.sessionCreate) - mcp.AddTool(server, &mcp.Tool{ + coremcp.AddToolRecorded(svc, server, "ide", &mcp.Tool{ Name: "ide_plan_status", Description: "Get the current plan status for a session", }, s.planStatus) diff --git a/pkg/mcp/ide/tools_dashboard.go b/pkg/mcp/ide/tools_dashboard.go index 5a58f09..0c14713 100644 --- a/pkg/mcp/ide/tools_dashboard.go +++ b/pkg/mcp/ide/tools_dashboard.go @@ -6,6 +6,7 @@ import ( "context" "time" + coremcp "dappco.re/go/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -85,18 +86,19 @@ type DashboardMetricsOutput struct { Metrics DashboardMetrics `json:"metrics"` } -func (s *Subsystem) registerDashboardTools(server *mcp.Server) { - mcp.AddTool(server, &mcp.Tool{ +func (s *Subsystem) registerDashboardTools(svc *coremcp.Service) { + server := svc.Server() + coremcp.AddToolRecorded(svc, server, "ide", &mcp.Tool{ Name: "ide_dashboard_overview", Description: "Get a high-level overview of the platform (repos, services, sessions, builds)", }, s.dashboardOverview) - mcp.AddTool(server, &mcp.Tool{ + coremcp.AddToolRecorded(svc, server, "ide", &mcp.Tool{ Name: "ide_dashboard_activity", Description: "Get the recent activity feed", }, s.dashboardActivity) - mcp.AddTool(server, &mcp.Tool{ + coremcp.AddToolRecorded(svc, server, "ide", &mcp.Tool{ Name: "ide_dashboard_metrics", Description: "Get aggregate build and agent metrics for a time period", }, s.dashboardMetrics) diff --git a/pkg/mcp/mcp.go b/pkg/mcp/mcp.go index 5f0982e..375f23b 100644 --- a/pkg/mcp/mcp.go +++ b/pkg/mcp/mcp.go @@ -130,7 +130,7 @@ func New(opts Options) (*Service, error) { svc.ChannelSend(ctx, channel, data) }) } - sub.RegisterTools(s.server) + sub.RegisterTools(s) } return s, nil diff --git a/pkg/mcp/registry.go b/pkg/mcp/registry.go index 030dd41..c84b72d 100644 --- a/pkg/mcp/registry.go +++ b/pkg/mcp/registry.go @@ -35,11 +35,17 @@ type ToolRecord struct { RESTHandler RESTHandler // REST-callable handler created at registration time } -// addToolRecorded registers a tool with the MCP server AND records its metadata. +// AddToolRecorded registers a tool with the MCP server and records its metadata. // This is a generic function that captures the In/Out types for schema extraction. // It also creates a RESTHandler closure that can unmarshal JSON to the correct // input type and call the handler directly, enabling the MCP-to-REST bridge. -func addToolRecorded[In, Out any](s *Service, server *mcp.Server, group string, t *mcp.Tool, h mcp.ToolHandlerFor[In, Out]) { +// +// svc, _ := mcp.New(mcp.Options{}) +// mcp.AddToolRecorded(svc, svc.Server(), "files", &mcp.Tool{Name: "file_read"}, +// func(context.Context, *mcp.CallToolRequest, ReadFileInput) (*mcp.CallToolResult, ReadFileOutput, error) { +// return nil, ReadFileOutput{Path: "src/main.go"}, nil +// }) +func AddToolRecorded[In, Out any](s *Service, server *mcp.Server, group string, t *mcp.Tool, h mcp.ToolHandlerFor[In, Out]) { mcp.AddTool(server, t, h) restHandler := func(ctx context.Context, body []byte) (any, error) { @@ -68,6 +74,10 @@ func addToolRecorded[In, Out any](s *Service, server *mcp.Server, group string, }) } +func addToolRecorded[In, Out any](s *Service, server *mcp.Server, group string, t *mcp.Tool, h mcp.ToolHandlerFor[In, Out]) { + AddToolRecorded(s, server, group, t, h) +} + // structSchema builds a simple JSON Schema from a struct's json tags via reflection. // Returns nil for non-struct types or empty structs. func structSchema(v any) map[string]any { diff --git a/pkg/mcp/subsystem.go b/pkg/mcp/subsystem.go index 3a26ac6..0ae5f17 100644 --- a/pkg/mcp/subsystem.go +++ b/pkg/mcp/subsystem.go @@ -4,8 +4,6 @@ package mcp import ( "context" - - "github.com/modelcontextprotocol/go-sdk/mcp" ) // Subsystem registers additional MCP tools at startup. @@ -13,10 +11,10 @@ import ( // // type BrainSubsystem struct{} // func (b *BrainSubsystem) Name() string { return "brain" } -// func (b *BrainSubsystem) RegisterTools(server *mcp.Server) { ... } +// func (b *BrainSubsystem) RegisterTools(svc *Service) { ... } type Subsystem interface { Name() string - RegisterTools(server *mcp.Server) + RegisterTools(svc *Service) } // SubsystemWithShutdown extends Subsystem with graceful cleanup. diff --git a/pkg/mcp/subsystem_test.go b/pkg/mcp/subsystem_test.go index 92bf0e9..c21d92f 100644 --- a/pkg/mcp/subsystem_test.go +++ b/pkg/mcp/subsystem_test.go @@ -3,8 +3,6 @@ package mcp import ( "context" "testing" - - "github.com/modelcontextprotocol/go-sdk/mcp" ) // stubSubsystem is a minimal Subsystem for testing. @@ -15,7 +13,7 @@ type stubSubsystem struct { func (s *stubSubsystem) Name() string { return s.name } -func (s *stubSubsystem) RegisterTools(server *mcp.Server) { +func (s *stubSubsystem) RegisterTools(svc *Service) { s.toolsRegistered = true } @@ -30,7 +28,7 @@ func (s *notifierSubsystem) SetNotifier(n Notifier) { s.notifierSet = n != nil } -func (s *notifierSubsystem) RegisterTools(server *mcp.Server) { +func (s *notifierSubsystem) RegisterTools(svc *Service) { s.sawNotifierAtRegistration = s.notifierSet s.toolsRegistered = true }