From 1c3930da96a06a767d91b06fcee48e2cdf13092b Mon Sep 17 00:00:00 2001 From: James Sesler Date: Thu, 23 Jul 2026 01:16:16 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20MCP=20parity=20=E2=80=94=20add=20open,?= =?UTF-8?q?=20usage,=20dashboard,=20list=5Fskills=20tools=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds four missing MCP tools that bring the MCP server to parity with Pi extension commands: - codecarto_open: activate existing workspace without resetting state - codecarto_usage: cumulative + per-phase token usage telemetry - codecarto_dashboard: regenerate dashboard.html - codecarto_list_skills: list available post-pipeline skills Also adds Pi-side /codecarto-library-list and /codecarto-library-reindex commands (MCP already had these as tools, Pi was missing them). --- mcp-server/server.ts | 105 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/mcp-server/server.ts b/mcp-server/server.ts index c3d8021..56f7918 100644 --- a/mcp-server/server.ts +++ b/mcp-server/server.ts @@ -28,6 +28,8 @@ import { buildValidationSummary, canonicalPath, completeValidatedPhase, + computePerPhaseTotals, + computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, @@ -46,6 +48,7 @@ import { listEntries, listSkillNames, loadCodecartoConfig, + loadUsage, loadYamlFile, normalizeForComparison, @@ -68,6 +71,7 @@ import { } from "../core/index.ts"; import { initLibrary } from "../core/library.ts"; import { loadUserConfig, resolveUserConfigPath } from "../core/orchestrator-config.ts"; +import { writeDashboard } from "../extensions/codecarto/dashboard-writer.ts"; // ---------- input helpers ---------- @@ -724,6 +728,67 @@ export async function handleConfig(args: { cwd?: string }) { ); } +// ---------- MCP parity handlers: open, usage, dashboard, list_skills ---------- + +export async function handleOpen(args: { cwd: string }) { + const cwd = await validateCwd(args.cwd); + const workspaceDir = join(cwd, ".codecarto"); + if (!(await pathExists(join(workspaceDir, "workflow", "status.yaml")))) { + throw new McpError(ErrorCode.InvalidRequest, "No existing CodeCartographer workspace found. Run codecarto_init first."); + } + const state = await requireWorkspace(cwd); + const nextPhase = getNextEligiblePhase(state)?.id ?? "complete"; + return textResult( + `Opened existing CodeCartographer workspace: ${getPipelineLabel(state.status.pipeline)}. Current phase: ${nextPhase}.`, + { pipeline: getPipelineLabel(state.status.pipeline), currentPhase: nextPhase }, + ); +} + +export async function handleUsage(args: { cwd: string }) { + const cwd = await validateCwd(args.cwd); + const state = await requireWorkspace(cwd); + const usage = await loadUsage(state.workspaceDir); + if (usage.runs.length === 0) { + return textResult("No phase runs recorded yet.", { runs: 0 }); + } + const totals = computeTotals(usage); + const perPhase = computePerPhaseTotals(usage); + const lines: string[] = [ + `Total runs: ${totals.runs}`, + `Total tokens: ${totals.tokens.input} in / ${totals.tokens.output} out / ${totals.tokens.cache_write} cache-write`, + `Total duration: ${totals.duration_ms}ms / ${totals.tool_uses} tool uses`, + "", + "Per-phase totals:", + ]; + for (const [phaseId, t] of perPhase) { + lines.push(` ${phaseId}: ${t.runs} run(s), ${t.tokens.input + t.tokens.output} tokens, ${t.tool_uses} tool uses, ${t.duration_ms}ms`); + } + return textResult(lines.join("\n"), { + runs: totals.runs, + tokens: totals.tokens, + toolUses: totals.tool_uses, + durationMs: totals.duration_ms, + perPhase: Object.fromEntries(perPhase), + }); +} + +export async function handleDashboard(args: { cwd: string }) { + const cwd = await validateCwd(args.cwd); + await requireWorkspace(cwd); + await writeDashboard(cwd, PACKAGE_VERSION); + return textResult("Dashboard regenerated: .codecarto/dashboard.html", { path: ".codecarto/dashboard.html" }); +} + +export async function handleListSkills(args: { cwd: string }) { + const cwd = await validateCwd(args.cwd); + const state = await requireWorkspace(cwd); + const skills = await listSkillNames(state.workspaceDir); + const lines = skills.length > 0 + ? [`Available skills (${skills.length}):`, ...skills.map((s) => ` - ${s}`)] + : ["No skills installed."]; + return textResult(lines.join("\n"), { skills }); +} + // ---------- tool registry ---------- const TOOLS = [ @@ -939,6 +1004,42 @@ const TOOLS = [ }, }, }, + { + name: "codecarto_open", + description: "Activate an existing CodeCartographer workspace without resetting state. Returns the current pipeline and phase.", + inputSchema: { + type: "object", + properties: { cwd: { type: "string", description: "Absolute path to the target repository." } }, + required: ["cwd"], + }, + }, + { + name: "codecarto_usage", + description: "Show cumulative and per-phase token usage from local phase runs.", + inputSchema: { + type: "object", + properties: { cwd: { type: "string", description: "Absolute path to the target repository." } }, + required: ["cwd"], + }, + }, + { + name: "codecarto_dashboard", + description: "Regenerate .codecarto/dashboard.html from the current workspace state.", + inputSchema: { + type: "object", + properties: { cwd: { type: "string", description: "Absolute path to the target repository." } }, + required: ["cwd"], + }, + }, + { + name: "codecarto_list_skills", + description: "List available post-pipeline skills installed in the workspace.", + inputSchema: { + type: "object", + properties: { cwd: { type: "string", description: "Absolute path to the target repository." } }, + required: ["cwd"], + }, + }, ] as const; const HANDLERS: Record Promise> = { @@ -956,6 +1057,10 @@ const HANDLERS: Record Promise> = { codecarto_library_init: handleLibraryInit, codecarto_config: handleConfig, codecarto_vision: handleVision, + codecarto_open: handleOpen, + codecarto_usage: handleUsage, + codecarto_dashboard: handleDashboard, + codecarto_list_skills: handleListSkills, }; // ---------- server bootstrap ----------