From bfb5bf84e2f034b1e448016bf407fc35d6732157 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 07:25:48 +0000 Subject: [PATCH] feat(mcp): register built-in tool groups Co-Authored-By: Virgil --- pkg/mcp/mcp.go | 7 +++++++ pkg/mcp/mcp_test.go | 40 ++++++++++++++++++++++++++++++++++++ pkg/mcp/registry_test.go | 44 ++++++++++++++++++++++++++++++++++++++-- pkg/mcp/tools_metrics.go | 8 ++++---- pkg/mcp/tools_process.go | 12 +++++------ pkg/mcp/tools_rag.go | 6 +++--- pkg/mcp/tools_webview.go | 20 +++++++++--------- pkg/mcp/tools_ws.go | 4 ++-- 8 files changed, 114 insertions(+), 27 deletions(-) diff --git a/pkg/mcp/mcp.go b/pkg/mcp/mcp.go index cf30975..2c75f90 100644 --- a/pkg/mcp/mcp.go +++ b/pkg/mcp/mcp.go @@ -256,6 +256,13 @@ func (s *Service) registerTools(server *mcp.Server) { Name: "lang_list", Description: "Get list of supported programming languages", }, s.getSupportedLanguages) + + // Additional built-in tool groups. + s.registerMetricsTools(server) + s.registerRAGTools(server) + s.registerProcessTools(server) + s.registerWebviewTools(server) + s.registerWSTools(server) } // Tool input/output types for MCP file operations. diff --git a/pkg/mcp/mcp_test.go b/pkg/mcp/mcp_test.go index d95beb1..dbb15d5 100644 --- a/pkg/mcp/mcp_test.go +++ b/pkg/mcp/mcp_test.go @@ -55,6 +55,46 @@ func TestNew_Good_NoRestriction(t *testing.T) { } } +func TestNew_Good_RegistersBuiltInTools(t *testing.T) { + s, err := New(Options{}) + if err != nil { + t.Fatalf("Failed to create service: %v", err) + } + + tools := map[string]bool{} + for _, rec := range s.Tools() { + tools[rec.Name] = true + } + + for _, name := range []string{ + "metrics_record", + "metrics_query", + "rag_query", + "rag_ingest", + "rag_collections", + "webview_connect", + "webview_disconnect", + "webview_navigate", + "webview_click", + "webview_type", + "webview_query", + "webview_console", + "webview_eval", + "webview_screenshot", + "webview_wait", + } { + if !tools[name] { + t.Fatalf("expected tool %q to be registered", name) + } + } + + for _, name := range []string{"process_start", "ws_start"} { + if tools[name] { + t.Fatalf("did not expect tool %q to be registered without dependencies", name) + } + } +} + func TestMedium_Good_ReadWrite(t *testing.T) { tmpDir := t.TempDir() s, err := New(Options{WorkspaceRoot: tmpDir}) diff --git a/pkg/mcp/registry_test.go b/pkg/mcp/registry_test.go index 36f5ce6..0686fe5 100644 --- a/pkg/mcp/registry_test.go +++ b/pkg/mcp/registry_test.go @@ -68,8 +68,12 @@ func TestToolRegistry_Good_ToolCount(t *testing.T) { tools := svc.Tools() // Built-in tools: file_read, file_write, file_delete, file_rename, - // file_exists, file_edit, dir_list, dir_create, lang_detect, lang_list - const expectedCount = 10 + // file_exists, file_edit, dir_list, dir_create, lang_detect, lang_list, + // metrics_record, metrics_query, rag_query, rag_ingest, rag_collections, + // webview_connect, webview_disconnect, webview_navigate, webview_click, + // webview_type, webview_query, webview_console, webview_eval, + // webview_screenshot, webview_wait + const expectedCount = 25 if len(tools) != expectedCount { t.Errorf("expected %d tools, got %d", expectedCount, len(tools)) for _, tr := range tools { @@ -86,6 +90,9 @@ func TestToolRegistry_Good_GroupAssignment(t *testing.T) { fileTools := []string{"file_read", "file_write", "file_delete", "file_rename", "file_exists", "file_edit", "dir_list", "dir_create"} langTools := []string{"lang_detect", "lang_list"} + metricsTools := []string{"metrics_record", "metrics_query"} + ragTools := []string{"rag_query", "rag_ingest", "rag_collections"} + webviewTools := []string{"webview_connect", "webview_disconnect", "webview_navigate", "webview_click", "webview_type", "webview_query", "webview_console", "webview_eval", "webview_screenshot", "webview_wait"} byName := make(map[string]ToolRecord) for _, tr := range svc.Tools() { @@ -113,6 +120,39 @@ func TestToolRegistry_Good_GroupAssignment(t *testing.T) { t.Errorf("tool %s: expected group 'language', got %q", name, tr.Group) } } + + for _, name := range metricsTools { + tr, ok := byName[name] + if !ok { + t.Errorf("tool %s not found in registry", name) + continue + } + if tr.Group != "metrics" { + t.Errorf("tool %s: expected group 'metrics', got %q", name, tr.Group) + } + } + + for _, name := range ragTools { + tr, ok := byName[name] + if !ok { + t.Errorf("tool %s not found in registry", name) + continue + } + if tr.Group != "rag" { + t.Errorf("tool %s: expected group 'rag', got %q", name, tr.Group) + } + } + + for _, name := range webviewTools { + tr, ok := byName[name] + if !ok { + t.Errorf("tool %s not found in registry", name) + continue + } + if tr.Group != "webview" { + t.Errorf("tool %s: expected group 'webview', got %q", name, tr.Group) + } + } } func TestToolRegistry_Good_ToolRecordFields(t *testing.T) { diff --git a/pkg/mcp/tools_metrics.go b/pkg/mcp/tools_metrics.go index a07302a..eb47491 100644 --- a/pkg/mcp/tools_metrics.go +++ b/pkg/mcp/tools_metrics.go @@ -5,8 +5,8 @@ import ( "strconv" "time" - "forge.lthn.ai/core/go-ai/ai" core "dappco.re/go/core" + "forge.lthn.ai/core/go-ai/ai" "forge.lthn.ai/core/go-log" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -71,19 +71,19 @@ type MetricCount struct { // // ev.Type == "dispatch.complete", ev.AgentID == "cladius", ev.Repo == "core-php" type MetricEventBrief struct { Type string `json:"type"` // e.g. "dispatch.complete" - Timestamp time.Time `json:"timestamp"` // when the event occurred + Timestamp time.Time `json:"timestamp"` // when the event occurred AgentID string `json:"agent_id,omitempty"` // e.g. "cladius" Repo string `json:"repo,omitempty"` // e.g. "core-php" } // registerMetricsTools adds metrics tools to the MCP server. func (s *Service) registerMetricsTools(server *mcp.Server) { - mcp.AddTool(server, &mcp.Tool{ + addToolRecorded(s, server, "metrics", &mcp.Tool{ Name: "metrics_record", Description: "Record a metrics event for AI/security tracking. Events are stored in daily JSONL files.", }, s.metricsRecord) - mcp.AddTool(server, &mcp.Tool{ + addToolRecorded(s, server, "metrics", &mcp.Tool{ Name: "metrics_query", Description: "Query metrics events and get aggregated statistics by type, repo, and agent.", }, s.metricsQuery) diff --git a/pkg/mcp/tools_process.go b/pkg/mcp/tools_process.go index 657f3bb..ccc948e 100644 --- a/pkg/mcp/tools_process.go +++ b/pkg/mcp/tools_process.go @@ -139,32 +139,32 @@ func (s *Service) registerProcessTools(server *mcp.Server) bool { return false } - mcp.AddTool(server, &mcp.Tool{ + addToolRecorded(s, server, "process", &mcp.Tool{ Name: "process_start", Description: "Start a new external process. Returns process ID for tracking.", }, s.processStart) - mcp.AddTool(server, &mcp.Tool{ + addToolRecorded(s, server, "process", &mcp.Tool{ Name: "process_stop", Description: "Gracefully stop a running process by ID.", }, s.processStop) - mcp.AddTool(server, &mcp.Tool{ + addToolRecorded(s, server, "process", &mcp.Tool{ Name: "process_kill", Description: "Force kill a process by ID. Use when process_stop doesn't work.", }, s.processKill) - mcp.AddTool(server, &mcp.Tool{ + addToolRecorded(s, server, "process", &mcp.Tool{ Name: "process_list", Description: "List all managed processes. Use running_only=true for only active processes.", }, s.processList) - mcp.AddTool(server, &mcp.Tool{ + addToolRecorded(s, server, "process", &mcp.Tool{ Name: "process_output", Description: "Get the captured output of a process by ID.", }, s.processOutput) - mcp.AddTool(server, &mcp.Tool{ + addToolRecorded(s, server, "process", &mcp.Tool{ Name: "process_input", Description: "Send input to a running process stdin.", }, s.processInput) diff --git a/pkg/mcp/tools_rag.go b/pkg/mcp/tools_rag.go index 39ca128..ceeaf29 100644 --- a/pkg/mcp/tools_rag.go +++ b/pkg/mcp/tools_rag.go @@ -99,17 +99,17 @@ type RAGCollectionsOutput struct { // registerRAGTools adds RAG tools to the MCP server. func (s *Service) registerRAGTools(server *mcp.Server) { - mcp.AddTool(server, &mcp.Tool{ + addToolRecorded(s, server, "rag", &mcp.Tool{ Name: "rag_query", Description: "Query the RAG vector database for relevant documentation. Returns semantically similar content based on the query.", }, s.ragQuery) - mcp.AddTool(server, &mcp.Tool{ + addToolRecorded(s, server, "rag", &mcp.Tool{ Name: "rag_ingest", Description: "Ingest documents into the RAG vector database. Supports both single files and directories.", }, s.ragIngest) - mcp.AddTool(server, &mcp.Tool{ + addToolRecorded(s, server, "rag", &mcp.Tool{ Name: "rag_collections", Description: "List all available collections in the RAG vector database.", }, s.ragCollections) diff --git a/pkg/mcp/tools_webview.go b/pkg/mcp/tools_webview.go index 182af6e..b4aee9c 100644 --- a/pkg/mcp/tools_webview.go +++ b/pkg/mcp/tools_webview.go @@ -201,52 +201,52 @@ type WebviewDisconnectOutput struct { // registerWebviewTools adds webview tools to the MCP server. func (s *Service) registerWebviewTools(server *mcp.Server) { - mcp.AddTool(server, &mcp.Tool{ + addToolRecorded(s, server, "webview", &mcp.Tool{ Name: "webview_connect", Description: "Connect to Chrome DevTools Protocol. Start Chrome with --remote-debugging-port=9222 first.", }, s.webviewConnect) - mcp.AddTool(server, &mcp.Tool{ + addToolRecorded(s, server, "webview", &mcp.Tool{ Name: "webview_disconnect", Description: "Disconnect from Chrome DevTools.", }, s.webviewDisconnect) - mcp.AddTool(server, &mcp.Tool{ + addToolRecorded(s, server, "webview", &mcp.Tool{ Name: "webview_navigate", Description: "Navigate the browser to a URL.", }, s.webviewNavigate) - mcp.AddTool(server, &mcp.Tool{ + addToolRecorded(s, server, "webview", &mcp.Tool{ Name: "webview_click", Description: "Click on an element by CSS selector.", }, s.webviewClick) - mcp.AddTool(server, &mcp.Tool{ + addToolRecorded(s, server, "webview", &mcp.Tool{ Name: "webview_type", Description: "Type text into an element by CSS selector.", }, s.webviewType) - mcp.AddTool(server, &mcp.Tool{ + addToolRecorded(s, server, "webview", &mcp.Tool{ Name: "webview_query", Description: "Query DOM elements by CSS selector.", }, s.webviewQuery) - mcp.AddTool(server, &mcp.Tool{ + addToolRecorded(s, server, "webview", &mcp.Tool{ Name: "webview_console", Description: "Get browser console output.", }, s.webviewConsole) - mcp.AddTool(server, &mcp.Tool{ + addToolRecorded(s, server, "webview", &mcp.Tool{ Name: "webview_eval", Description: "Evaluate JavaScript in the browser context.", }, s.webviewEval) - mcp.AddTool(server, &mcp.Tool{ + addToolRecorded(s, server, "webview", &mcp.Tool{ Name: "webview_screenshot", Description: "Capture a screenshot of the browser window.", }, s.webviewScreenshot) - mcp.AddTool(server, &mcp.Tool{ + addToolRecorded(s, server, "webview", &mcp.Tool{ Name: "webview_wait", Description: "Wait for an element to appear by CSS selector.", }, s.webviewWait) diff --git a/pkg/mcp/tools_ws.go b/pkg/mcp/tools_ws.go index b5ad635..df0bd62 100644 --- a/pkg/mcp/tools_ws.go +++ b/pkg/mcp/tools_ws.go @@ -47,12 +47,12 @@ func (s *Service) registerWSTools(server *mcp.Server) bool { return false } - mcp.AddTool(server, &mcp.Tool{ + addToolRecorded(s, server, "ws", &mcp.Tool{ Name: "ws_start", Description: "Start the WebSocket server for real-time process output streaming.", }, s.wsStart) - mcp.AddTool(server, &mcp.Tool{ + addToolRecorded(s, server, "ws", &mcp.Tool{ Name: "ws_info", Description: "Get WebSocket hub statistics (connected clients and active channels).", }, s.wsInfo)