feat(mcp): register built-in tool groups

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 07:25:48 +00:00
parent 20caaebc21
commit bfb5bf84e2
8 changed files with 114 additions and 27 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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