refactor(mcp): record subsystem tools centrally

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 17:11:16 +00:00
parent a215428df8
commit da30f3144a
23 changed files with 150 additions and 106 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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.

View file

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

View file

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

View file

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

View file

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

View file

@ -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) {

View file

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

View file

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

View file

@ -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) {

View file

@ -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.

View file

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

View file

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

View file

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

View file

@ -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

View file

@ -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 {

View file

@ -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.

View file

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