@dev-tren/mapd 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/MASTER_PROMPT.md +134 -0
- package/README.md +494 -0
- package/SETUP.md +108 -0
- package/UAT.md +77 -0
- package/package.json +56 -0
- package/src/adapters/github-app.js +79 -0
- package/src/agents/anthropicClient.js +18 -0
- package/src/agents/llm.js +196 -0
- package/src/agents/modelResolver.js +87 -0
- package/src/agents/provider.js +222 -0
- package/src/chat/commandRunner.js +86 -0
- package/src/chat/commands.js +275 -0
- package/src/chat/intent.js +87 -0
- package/src/chat/llmIntent.js +118 -0
- package/src/chat/repl.js +471 -0
- package/src/cli.js +1408 -0
- package/src/config/index.js +197 -0
- package/src/config/schema.js +119 -0
- package/src/core/assist.js +64 -0
- package/src/core/audit.js +63 -0
- package/src/core/changes.js +110 -0
- package/src/core/confidence.js +0 -0
- package/src/core/configLint.js +141 -0
- package/src/core/diagnose.js +262 -0
- package/src/core/docs.js +140 -0
- package/src/core/doctor.js +134 -0
- package/src/core/envFiles.js +43 -0
- package/src/core/events.js +53 -0
- package/src/core/evidence.js +212 -0
- package/src/core/findingScoring.js +20 -0
- package/src/core/fix.js +192 -0
- package/src/core/fixApply.js +172 -0
- package/src/core/frameworkEntries.js +247 -0
- package/src/core/gates.js +209 -0
- package/src/core/graph.js +467 -0
- package/src/core/grounding.js +235 -0
- package/src/core/handoff.js +157 -0
- package/src/core/importResolver.js +218 -0
- package/src/core/improve.js +226 -0
- package/src/core/integrate.js +169 -0
- package/src/core/intelligence.js +212 -0
- package/src/core/modernize.js +370 -0
- package/src/core/parseCache.js +64 -0
- package/src/core/parser.js +536 -0
- package/src/core/policy.js +65 -0
- package/src/core/polyglot.js +333 -0
- package/src/core/proc.js +25 -0
- package/src/core/reachability.js +543 -0
- package/src/core/regression.js +193 -0
- package/src/core/resolution.js +92 -0
- package/src/core/retry.js +61 -0
- package/src/core/review.js +219 -0
- package/src/core/score.js +338 -0
- package/src/core/security.js +0 -0
- package/src/core/session.js +143 -0
- package/src/core/solutions.js +254 -0
- package/src/core/staleness.js +45 -0
- package/src/core/testGuidance.js +226 -0
- package/src/core/theme.js +50 -0
- package/src/core/trace.js +151 -0
- package/src/core/verify.js +123 -0
- package/src/core/view.js +221 -0
- package/src/core/viewServer.js +88 -0
- package/src/core/watch.js +76 -0
- package/src/core/workspace.js +115 -0
- package/src/mcp/server.js +48 -0
- package/src/mcp/tools.js +423 -0
- package/src/server.js +84 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* watch.js — the reusable watch loop (hash-cached remap on file change,
|
|
3
|
+
* structured events) shared by `mapd watch` and chat's in-session watch
|
|
4
|
+
* subscription. Only one implementation of the fs.watch/debounce/remap
|
|
5
|
+
* logic exists — cli.js and chat/repl.js both consume it.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { parseProject, ALWAYS_IGNORE } from "./parser.js";
|
|
11
|
+
import { buildGraph, loadPkg } from "./graph.js";
|
|
12
|
+
import { scoreGraph } from "./confidence.js";
|
|
13
|
+
import { diffGraphs } from "./regression.js";
|
|
14
|
+
import { createWatchBus, buildRescanEvent, emitRescanEvent } from "./events.js";
|
|
15
|
+
import { loadConfig } from "../config/index.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Starts watching `rootDir`. Returns `{ bus, initialGraph, stop() }`.
|
|
19
|
+
* `bus` emits: "remap", "regression", "finding", "resolved" (see events.js).
|
|
20
|
+
* Never throws on transient parse errors during a rescan — reports them and
|
|
21
|
+
* keeps watching, matching `mapd watch`'s existing behavior.
|
|
22
|
+
*/
|
|
23
|
+
export function startWatcher(rootDir, { intervalMs = 400, bus = createWatchBus() } = {}) {
|
|
24
|
+
const abs = path.resolve(rootDir);
|
|
25
|
+
const config = loadConfig(abs);
|
|
26
|
+
const cache = new Map();
|
|
27
|
+
const build = () => {
|
|
28
|
+
const parsed = parseProject(abs, {
|
|
29
|
+
cache,
|
|
30
|
+
ignore: ALWAYS_IGNORE,
|
|
31
|
+
include: config.project?.include ?? [],
|
|
32
|
+
exclude: config.project?.exclude ?? [],
|
|
33
|
+
maxFileSizeBytes: config.mapping?.maxFileSizeBytes ?? Infinity,
|
|
34
|
+
polyglot: config.mapping?.polyglot !== false,
|
|
35
|
+
});
|
|
36
|
+
const g = scoreGraph(abs, buildGraph(abs, parsed, loadPkg(abs), { annotations: config.project?.annotations ?? {} }));
|
|
37
|
+
return { g, parsed };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
let { g: prev } = build();
|
|
41
|
+
const initialGraph = prev;
|
|
42
|
+
let timer = null;
|
|
43
|
+
|
|
44
|
+
const rescan = () => {
|
|
45
|
+
const t0 = Date.now();
|
|
46
|
+
let g, parsed;
|
|
47
|
+
try {
|
|
48
|
+
({ g, parsed } = build());
|
|
49
|
+
} catch (e) {
|
|
50
|
+
bus.emit("error", e);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const durationMs = Date.now() - t0;
|
|
54
|
+
const findings = diffGraphs(prev, g);
|
|
55
|
+
emitRescanEvent(bus, buildRescanEvent({ durationMs, parsed, prevGraph: prev, newGraph: g, findings }));
|
|
56
|
+
prev = g;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const watcher = fs.watch(abs, { recursive: true }, (_event, filename) => {
|
|
60
|
+
if (!filename) return;
|
|
61
|
+
const n = filename.toString();
|
|
62
|
+
if (n.includes("node_modules") || n.startsWith(".mapd") || n.startsWith(".git")) return;
|
|
63
|
+
if (!/\.(js|jsx|ts|tsx|mjs|cjs|mts|cts|json)$/.test(n)) return;
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
timer = setTimeout(rescan, Math.max(50, intervalMs));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
bus,
|
|
70
|
+
initialGraph,
|
|
71
|
+
stop() {
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
watcher.close();
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workspace.js — patch isolation. Never apply an unverified proposal directly
|
|
3
|
+
* to the user's real working tree.
|
|
4
|
+
*
|
|
5
|
+
* Two isolation strategies, same surface:
|
|
6
|
+
* - "worktree" — git-backed projects: reuses the exact `mkdtempSync` +
|
|
7
|
+
* `git worktree add --detach` pattern already proven in
|
|
8
|
+
* integrate.js's detectConflicts().
|
|
9
|
+
* - "tmpdir-copy" — non-git projects: a plain recursive filesystem copy,
|
|
10
|
+
* excluding .git/.mapd/node_modules.
|
|
11
|
+
*
|
|
12
|
+
* Callers (fix.js, mcp/tools.js) never branch on git-vs-not — they only see
|
|
13
|
+
* `{dir, kind, cleanup()}` plus applyPatchInWorkspace/diffWorkspace/promoteWorkspaceChange.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { execFileSync } from "node:child_process";
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import os from "node:os";
|
|
20
|
+
import { sanitizeRelPath as sanitizeWithinRoot } from "./security.js";
|
|
21
|
+
|
|
22
|
+
function git(cwd, ...args) {
|
|
23
|
+
return execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] }).toString();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isGitRepo(rootDir) {
|
|
27
|
+
try {
|
|
28
|
+
return git(rootDir, "rev-parse", "--is-inside-work-tree").trim() === "true";
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const IGNORE_DIRS = new Set(["node_modules", ".git", ".mapd"]);
|
|
35
|
+
|
|
36
|
+
function copyTree(src, dest) {
|
|
37
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
38
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
39
|
+
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
40
|
+
const s = path.join(src, entry.name);
|
|
41
|
+
const d = path.join(dest, entry.name);
|
|
42
|
+
if (entry.isSymbolicLink()) continue; // never follow symlinks out of the tree
|
|
43
|
+
if (entry.isDirectory()) copyTree(s, d);
|
|
44
|
+
else fs.copyFileSync(s, d);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Guard against path traversal / symlink escape out of the workspace root. */
|
|
49
|
+
export function sanitizeRelPath(workspaceDir, relFile) {
|
|
50
|
+
return sanitizeWithinRoot(workspaceDir, relFile);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Create an isolated copy of rootDir to safely apply and verify an unverified patch. */
|
|
54
|
+
export function createIsolatedWorkspace(rootDir) {
|
|
55
|
+
const abs = path.resolve(rootDir);
|
|
56
|
+
if (isGitRepo(abs)) {
|
|
57
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "mapd-ws-"));
|
|
58
|
+
const head = git(abs, "rev-parse", "--abbrev-ref", "HEAD").trim();
|
|
59
|
+
git(abs, "worktree", "add", "--detach", dir, head === "HEAD" ? git(abs, "rev-parse", "HEAD").trim() : head);
|
|
60
|
+
return {
|
|
61
|
+
dir, kind: "worktree", rootDir: abs,
|
|
62
|
+
cleanup() {
|
|
63
|
+
try { git(abs, "worktree", "remove", "--force", dir); } catch { /* best effort */ }
|
|
64
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "mapd-ws-"));
|
|
69
|
+
copyTree(abs, dir);
|
|
70
|
+
return {
|
|
71
|
+
dir, kind: "tmpdir-copy", rootDir: abs,
|
|
72
|
+
cleanup() {
|
|
73
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Write a proposed patch's resulting source for one file inside the isolated workspace. */
|
|
79
|
+
export function applyPatchInWorkspace(ws, relFile, newSource) {
|
|
80
|
+
const abs = sanitizeRelPath(ws.dir, relFile);
|
|
81
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
82
|
+
fs.writeFileSync(abs, newSource);
|
|
83
|
+
return abs;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* List every file that differs between the isolated workspace and rootDir
|
|
88
|
+
* (relative paths). Used by G1 to enforce "only allowed files changed."
|
|
89
|
+
*/
|
|
90
|
+
export function diffWorkspace(ws) {
|
|
91
|
+
const changed = [];
|
|
92
|
+
const walk = (relDir) => {
|
|
93
|
+
const wsAbs = path.join(ws.dir, relDir);
|
|
94
|
+
if (!fs.existsSync(wsAbs)) return;
|
|
95
|
+
for (const entry of fs.readdirSync(wsAbs, { withFileTypes: true })) {
|
|
96
|
+
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
97
|
+
const rel = path.join(relDir, entry.name);
|
|
98
|
+
if (entry.isDirectory()) { walk(rel); continue; }
|
|
99
|
+
const wsFile = path.join(ws.dir, rel);
|
|
100
|
+
const rootFile = path.join(ws.rootDir, rel);
|
|
101
|
+
const wsContent = fs.readFileSync(wsFile);
|
|
102
|
+
let rootContent = null;
|
|
103
|
+
try { rootContent = fs.readFileSync(rootFile); } catch { /* new file */ }
|
|
104
|
+
if (!rootContent || !wsContent.equals(rootContent)) changed.push(rel.split(path.sep).join("/"));
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
walk(".");
|
|
108
|
+
return changed;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Read back the (already gate-verified) content of one file from the workspace. */
|
|
112
|
+
export function promoteWorkspaceChange(ws, relFile) {
|
|
113
|
+
const abs = sanitizeRelPath(ws.dir, relFile);
|
|
114
|
+
return fs.readFileSync(abs, "utf8");
|
|
115
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server.js — `mapd mcp`: a local MCP server over stdio, exposing Map'd's
|
|
3
|
+
* project-understanding and verification layer to any MCP-compatible agent
|
|
4
|
+
* (Claude Code, Cursor, MITRI, ...). No GitHub required; works over stdio
|
|
5
|
+
* against a local project directory.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
10
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
12
|
+
import { createToolHandlers } from "./tools.js";
|
|
13
|
+
|
|
14
|
+
const pkgVersion = JSON.parse(fs.readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
|
|
15
|
+
|
|
16
|
+
export function createMcpServer() {
|
|
17
|
+
const tools = createToolHandlers();
|
|
18
|
+
const server = new Server({ name: "mapd", version: pkgVersion }, { capabilities: { tools: {} } });
|
|
19
|
+
|
|
20
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
21
|
+
tools: Object.entries(tools).map(([name, def]) => ({
|
|
22
|
+
name, description: def.description, inputSchema: def.inputSchema,
|
|
23
|
+
})),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
27
|
+
const { name, arguments: args = {} } = request.params;
|
|
28
|
+
const tool = tools[name];
|
|
29
|
+
if (!tool) {
|
|
30
|
+
return { isError: true, content: [{ type: "text", text: JSON.stringify({ ok: false, denied: true, reason: `Unknown tool: ${name}` }) }] };
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const result = await tool.handler(args);
|
|
34
|
+
return { isError: result.ok === false, content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
35
|
+
} catch (e) {
|
|
36
|
+
return { isError: true, content: [{ type: "text", text: JSON.stringify({ ok: false, denied: true, reason: e.message }) }] };
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return server;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function startMcpServer() {
|
|
44
|
+
const server = createMcpServer();
|
|
45
|
+
const transport = new StdioServerTransport();
|
|
46
|
+
await server.connect(transport);
|
|
47
|
+
return server;
|
|
48
|
+
}
|
package/src/mcp/tools.js
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tools.js — MCP tool table. Every handler delegates to the exact same core
|
|
3
|
+
* services cli.js and chat/commands.js use — MCP is a presentation layer,
|
|
4
|
+
* never a parallel logic path.
|
|
5
|
+
*
|
|
6
|
+
* Read-only tools run directly. The one true real-tree mutation
|
|
7
|
+
* (apply_approved_fix) requires an explicit `approve: true` argument from
|
|
8
|
+
* the calling agent and still goes through the shared fix-apply safety layer,
|
|
9
|
+
* so it can never bypass proposal gates, change recording, or rollback checks.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import { performance } from "node:perf_hooks";
|
|
15
|
+
import { buildScoredGraph, getRepoStatusSummary, getWorkflowSummaries, findFileNode, searchFunctions, buildTaskContext } from "../core/intelligence.js";
|
|
16
|
+
import { loadBaseline, diffGraphs } from "../core/regression.js";
|
|
17
|
+
import { renderDocs } from "../core/docs.js";
|
|
18
|
+
import { runModernizationScan, saveModernizationReport } from "../core/modernize.js";
|
|
19
|
+
import { loadPkg } from "../core/graph.js";
|
|
20
|
+
import { loadQueue, pending, loadQueueWithStates } from "../core/review.js";
|
|
21
|
+
import { runFixLifecycle } from "../core/fix.js";
|
|
22
|
+
import { chooseFixTarget, approveFixWithPostApplyVerification } from "../core/fixApply.js";
|
|
23
|
+
import { buildDiagnosis } from "../core/diagnose.js";
|
|
24
|
+
import { buildHandoff, renderHandoffPrompt } from "../core/handoff.js";
|
|
25
|
+
import { buildSolutions } from "../core/solutions.js";
|
|
26
|
+
import { buildFindingEvidence } from "../core/evidence.js";
|
|
27
|
+
import { loadChanges, rollbackChange } from "../core/changes.js";
|
|
28
|
+
import { loadConfig, listAnnotations } from "../config/index.js";
|
|
29
|
+
import { applyAnnotations } from "../core/reachability.js";
|
|
30
|
+
import { recordAudit } from "../core/audit.js";
|
|
31
|
+
import { sanitizeRelPath } from "../core/security.js";
|
|
32
|
+
|
|
33
|
+
function denied(reason, extra = {}) {
|
|
34
|
+
return { ok: false, denied: true, reason, ...extra };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveProjectRoot(dir) {
|
|
38
|
+
// MCP clients may pass any string; always resolve within the filesystem
|
|
39
|
+
// normally (path.resolve), then let downstream fs calls fail naturally if
|
|
40
|
+
// the directory doesn't exist — no execution of untrusted paths as commands.
|
|
41
|
+
return path.resolve(dir ?? ".");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createToolHandlers() {
|
|
45
|
+
return {
|
|
46
|
+
map_project: {
|
|
47
|
+
description: "Build and return the current scored workflow map (read-only).",
|
|
48
|
+
inputSchema: { type: "object", properties: { dir: { type: "string" } }, required: [] },
|
|
49
|
+
mutation: false,
|
|
50
|
+
async handler({ dir }) {
|
|
51
|
+
const g = buildScoredGraph(resolveProjectRoot(dir));
|
|
52
|
+
return { ok: true, stats: g.stats, repoConfidence: g.repoConfidence, workflows: getWorkflowSummaries(g), orphans: g.orphans };
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
get_project_summary: {
|
|
57
|
+
description: "Return a compact project summary (files, workflows, confidence, orphan count).",
|
|
58
|
+
inputSchema: { type: "object", properties: { dir: { type: "string" } }, required: [] },
|
|
59
|
+
mutation: false,
|
|
60
|
+
async handler({ dir }) {
|
|
61
|
+
const g = buildScoredGraph(resolveProjectRoot(dir));
|
|
62
|
+
return { ok: true, summary: getRepoStatusSummary(g) };
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
get_project_diagnosis: {
|
|
67
|
+
description: "Return Map'd's deterministic diagnosis of understanding limits: confidence blockers, runtime blind spots, env contract, and next actions.",
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: "object",
|
|
70
|
+
properties: {
|
|
71
|
+
dir: { type: "string" },
|
|
72
|
+
top: { type: "number" },
|
|
73
|
+
},
|
|
74
|
+
required: [],
|
|
75
|
+
},
|
|
76
|
+
mutation: false,
|
|
77
|
+
async handler({ dir, top }) {
|
|
78
|
+
return { ok: true, diagnosis: buildDiagnosis(resolveProjectRoot(dir), { top: top ?? 5 }) };
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
profile_project: {
|
|
83
|
+
description: "Profile map-building and optionally modernization scan timing (read-only).",
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: {
|
|
87
|
+
dir: { type: "string" },
|
|
88
|
+
modernizeMode: { type: "string", enum: ["light", "medium", "heavy"] },
|
|
89
|
+
checkRegistry: { type: "boolean" },
|
|
90
|
+
registryTimeoutMs: { type: "number" },
|
|
91
|
+
},
|
|
92
|
+
required: [],
|
|
93
|
+
},
|
|
94
|
+
mutation: false,
|
|
95
|
+
async handler({ dir, modernizeMode, checkRegistry, registryTimeoutMs }) {
|
|
96
|
+
const abs = resolveProjectRoot(dir);
|
|
97
|
+
const startedAt = performance.now();
|
|
98
|
+
const g = buildScoredGraph(abs);
|
|
99
|
+
const mapDurationMs = Number((performance.now() - startedAt).toFixed(2));
|
|
100
|
+
const result = {
|
|
101
|
+
map: {
|
|
102
|
+
durationMs: mapDurationMs,
|
|
103
|
+
fileCount: g.stats.fileCount,
|
|
104
|
+
workflowCount: g.workflows.length,
|
|
105
|
+
repoConfidence: g.repoConfidence,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
if (modernizeMode) {
|
|
109
|
+
const report = runModernizationScan(abs, g, loadPkg(abs), modernizeMode, {
|
|
110
|
+
profile: true,
|
|
111
|
+
checkRegistry: checkRegistry !== false,
|
|
112
|
+
registryTimeoutMs: registryTimeoutMs ?? 2000,
|
|
113
|
+
});
|
|
114
|
+
result.modernize = { mode: modernizeMode, findingCount: report.findings.length, profile: report.profile };
|
|
115
|
+
}
|
|
116
|
+
return { ok: true, profile: result };
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
get_workflow: {
|
|
121
|
+
description: "Return one workflow's files, function count, and confidence signals by workflow ID.",
|
|
122
|
+
inputSchema: { type: "object", properties: { dir: { type: "string" }, workflowId: { type: "string" } }, required: ["workflowId"] },
|
|
123
|
+
mutation: false,
|
|
124
|
+
async handler({ dir, workflowId }) {
|
|
125
|
+
const g = buildScoredGraph(resolveProjectRoot(dir));
|
|
126
|
+
const wf = g.workflows.find((w) => w.id === workflowId);
|
|
127
|
+
if (!wf) return denied(`No workflow with id ${workflowId}`);
|
|
128
|
+
return { ok: true, workflow: wf };
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
search_project: {
|
|
133
|
+
description: "Keyword search over functions and files (deterministic ranking, not a full-repo dump).",
|
|
134
|
+
inputSchema: { type: "object", properties: { dir: { type: "string" }, query: { type: "string" } }, required: ["query"] },
|
|
135
|
+
mutation: false,
|
|
136
|
+
async handler({ dir, query }) {
|
|
137
|
+
const g = buildScoredGraph(resolveProjectRoot(dir));
|
|
138
|
+
return { ok: true, hits: searchFunctions(g, query).slice(0, 20) };
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
get_task_context: {
|
|
143
|
+
description: "Return a compact graph-backed context pack for a task/question: ranked symbols, relevant files, workflows, and caveats.",
|
|
144
|
+
inputSchema: {
|
|
145
|
+
type: "object",
|
|
146
|
+
properties: {
|
|
147
|
+
dir: { type: "string" },
|
|
148
|
+
query: { type: "string" },
|
|
149
|
+
maxHits: { type: "number" },
|
|
150
|
+
maxFiles: { type: "number" },
|
|
151
|
+
},
|
|
152
|
+
required: ["query"],
|
|
153
|
+
},
|
|
154
|
+
mutation: false,
|
|
155
|
+
async handler({ dir, query, maxHits, maxFiles }) {
|
|
156
|
+
const g = buildScoredGraph(resolveProjectRoot(dir));
|
|
157
|
+
return { ok: true, context: buildTaskContext(g, query, { maxHits: maxHits ?? 8, maxFiles: maxFiles ?? 8 }) };
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
get_symbol: {
|
|
162
|
+
description: "Return one file's parsed functions/imports/exports.",
|
|
163
|
+
inputSchema: { type: "object", properties: { dir: { type: "string" }, file: { type: "string" } }, required: ["file"] },
|
|
164
|
+
mutation: false,
|
|
165
|
+
async handler({ dir, file }) {
|
|
166
|
+
const abs = resolveProjectRoot(dir);
|
|
167
|
+
try { sanitizeRelPath(abs, file); } catch (e) { return denied(e.message); }
|
|
168
|
+
const g = buildScoredGraph(abs);
|
|
169
|
+
const node = findFileNode(g, file);
|
|
170
|
+
if (!node) return denied(`No such file in the map: ${file}`);
|
|
171
|
+
return { ok: true, file: node };
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
check_project: {
|
|
176
|
+
description: "Diff the current map against the saved baseline and write findings (mirrors `mapd check`; writes only Map'd metadata, not source files).",
|
|
177
|
+
inputSchema: { type: "object", properties: { dir: { type: "string" } }, required: [] },
|
|
178
|
+
mutation: false,
|
|
179
|
+
async handler({ dir }) {
|
|
180
|
+
const abs = resolveProjectRoot(dir);
|
|
181
|
+
const loaded = loadBaseline(abs);
|
|
182
|
+
if (!loaded) return denied("No baseline found — run `mapd baseline` first.");
|
|
183
|
+
if (loaded.schemaMismatch) return denied("Baseline schema mismatch — re-run `mapd baseline`.");
|
|
184
|
+
const current = buildScoredGraph(abs);
|
|
185
|
+
const findings = diffGraphs(loaded.graph, current);
|
|
186
|
+
return { ok: true, findings, repoConfidence: current.repoConfidence };
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
compare_baseline: {
|
|
191
|
+
description: "Read-only diff against the baseline without saving findings.",
|
|
192
|
+
inputSchema: { type: "object", properties: { dir: { type: "string" } }, required: [] },
|
|
193
|
+
mutation: false,
|
|
194
|
+
async handler({ dir }) {
|
|
195
|
+
const abs = resolveProjectRoot(dir);
|
|
196
|
+
const loaded = loadBaseline(abs);
|
|
197
|
+
if (!loaded) return denied("No baseline found.");
|
|
198
|
+
if (loaded.schemaMismatch) return denied("Baseline schema mismatch.");
|
|
199
|
+
const current = buildScoredGraph(abs);
|
|
200
|
+
return { ok: true, findings: diffGraphs(loaded.graph, current) };
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
modernize_project: {
|
|
205
|
+
description: "Run the rule-based modernization scan (mirrors `mapd modernize`).",
|
|
206
|
+
inputSchema: {
|
|
207
|
+
type: "object",
|
|
208
|
+
properties: {
|
|
209
|
+
dir: { type: "string" },
|
|
210
|
+
mode: { type: "string", enum: ["light", "medium", "heavy"] },
|
|
211
|
+
profile: { type: "boolean" },
|
|
212
|
+
checkRegistry: { type: "boolean" },
|
|
213
|
+
registryTimeoutMs: { type: "number" },
|
|
214
|
+
},
|
|
215
|
+
required: [],
|
|
216
|
+
},
|
|
217
|
+
mutation: false,
|
|
218
|
+
async handler({ dir, mode, profile, checkRegistry, registryTimeoutMs }) {
|
|
219
|
+
const abs = resolveProjectRoot(dir);
|
|
220
|
+
const g = buildScoredGraph(abs);
|
|
221
|
+
const report = runModernizationScan(abs, g, loadPkg(abs), mode ?? "medium", {
|
|
222
|
+
profile: !!profile,
|
|
223
|
+
checkRegistry: checkRegistry !== false,
|
|
224
|
+
registryTimeoutMs: registryTimeoutMs ?? 2000,
|
|
225
|
+
});
|
|
226
|
+
const fresh = report.findings; // before the save merges in carried lifecycle entries
|
|
227
|
+
const saved = saveModernizationReport(abs, report);
|
|
228
|
+
return { ok: true, findings: fresh, resolvedNow: saved.resolvedNow, profile: report.profile };
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
list_findings: {
|
|
233
|
+
description: "List review-queue items with derived states (active, stale, approved, resolved, dismissed, historical). By default returns only items awaiting approval; pass state to filter, or all:true for everything.",
|
|
234
|
+
inputSchema: {
|
|
235
|
+
type: "object",
|
|
236
|
+
properties: {
|
|
237
|
+
dir: { type: "string" },
|
|
238
|
+
state: { type: "string", enum: ["active", "stale", "approved", "resolved", "dismissed", "historical"] },
|
|
239
|
+
all: { type: "boolean" },
|
|
240
|
+
},
|
|
241
|
+
required: [],
|
|
242
|
+
},
|
|
243
|
+
mutation: false,
|
|
244
|
+
async handler({ dir, state, all }) {
|
|
245
|
+
const { items, freshness } = loadQueueWithStates(resolveProjectRoot(dir));
|
|
246
|
+
const filtered = state ? items.filter((i) => i.state === state) : all ? items : pending(items);
|
|
247
|
+
return { ok: true, items: filtered, freshness };
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
get_finding_evidence: {
|
|
252
|
+
description: "Return the deterministic evidence behind one review-queue item: files with workflow membership and reachability class, user annotations, gate results, and report freshness.",
|
|
253
|
+
inputSchema: { type: "object", properties: { dir: { type: "string" }, id: { type: "string" } }, required: ["id"] },
|
|
254
|
+
mutation: false,
|
|
255
|
+
async handler({ dir, id }) {
|
|
256
|
+
const evidence = buildFindingEvidence(resolveProjectRoot(dir), id);
|
|
257
|
+
if (!evidence) return denied(`No item with id ${id} — call list_findings for current IDs.`);
|
|
258
|
+
return { ok: true, evidence };
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
get_handoff: {
|
|
263
|
+
description: "Package the highest-priority open findings into an external-agent task prompt (mirrors `mapd handoff`). Stale-report findings are excluded, with the exclusion disclosed.",
|
|
264
|
+
inputSchema: { type: "object", properties: { dir: { type: "string" }, top: { type: "number" } }, required: [] },
|
|
265
|
+
mutation: false,
|
|
266
|
+
async handler({ dir, top }) {
|
|
267
|
+
const data = buildHandoff(resolveProjectRoot(dir), { top: top ?? 5 });
|
|
268
|
+
return { ok: true, handoff: data, prompt: renderHandoffPrompt(data) };
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
get_solutions: {
|
|
273
|
+
description: "Cluster related open findings into data-backed solutions ranked by workflow blast radius (mirrors `mapd solutions`, deterministic clustering only — no narration).",
|
|
274
|
+
inputSchema: { type: "object", properties: { dir: { type: "string" }, top: { type: "number" } }, required: [] },
|
|
275
|
+
mutation: false,
|
|
276
|
+
async handler({ dir, top }) {
|
|
277
|
+
return { ok: true, solutions: buildSolutions(resolveProjectRoot(dir), { top: top ?? 5 }) };
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
list_changes: {
|
|
282
|
+
description: "List every recorded real-tree change (fix/integrate/review/annotate writes) with backup metadata and rollback status.",
|
|
283
|
+
inputSchema: { type: "object", properties: { dir: { type: "string" } }, required: [] },
|
|
284
|
+
mutation: false,
|
|
285
|
+
async handler({ dir }) {
|
|
286
|
+
return { ok: true, changes: loadChanges(resolveProjectRoot(dir)) };
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
rollback_change: {
|
|
291
|
+
description: "Restore a file to its state before a recorded change. Requires approve:true from the calling agent — a real-tree mutation, same approval rule as apply_approved_fix.",
|
|
292
|
+
inputSchema: { type: "object", properties: { dir: { type: "string" }, changeId: { type: "string" }, approve: { type: "boolean" } }, required: ["changeId"] },
|
|
293
|
+
mutation: true,
|
|
294
|
+
async handler({ dir, changeId, approve }) {
|
|
295
|
+
const abs = resolveProjectRoot(dir);
|
|
296
|
+
if (approve !== true) {
|
|
297
|
+
return denied(
|
|
298
|
+
`rollback_change requires an explicit approve:true argument — mutation was NOT applied.`,
|
|
299
|
+
{ requiresApproval: true, changeId },
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
const r = rollbackChange(abs, changeId);
|
|
303
|
+
recordAudit(abs, { command: "mcp:rollback_change", initiator: "mcp", approvalStatus: r.ok ? "approved" : "failed", finalStatus: r.ok ? "rolled-back" : "failed", changeIds: [changeId] });
|
|
304
|
+
if (!r.ok) return denied(r.detail);
|
|
305
|
+
return { ok: true, detail: r.detail };
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
list_annotations: {
|
|
310
|
+
description: "List user-asserted annotations (.mapdrc project.annotations) and which files in the current map each pattern matches. These are user assertions, never auto-detected facts.",
|
|
311
|
+
inputSchema: { type: "object", properties: { dir: { type: "string" } }, required: [] },
|
|
312
|
+
mutation: false,
|
|
313
|
+
async handler({ dir }) {
|
|
314
|
+
const abs = resolveProjectRoot(dir);
|
|
315
|
+
const annotations = listAnnotations(abs);
|
|
316
|
+
const g = buildScoredGraph(abs);
|
|
317
|
+
const matches = applyAnnotations(new Set(g.files.map((f) => f.file)), annotations);
|
|
318
|
+
return {
|
|
319
|
+
ok: true,
|
|
320
|
+
annotations: Object.entries(annotations).map(([pattern, classification]) => ({
|
|
321
|
+
pattern, classification,
|
|
322
|
+
matchedFiles: matches.filter((m) => m.pattern === pattern).map((m) => m.file),
|
|
323
|
+
})),
|
|
324
|
+
};
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
get_finding: {
|
|
329
|
+
description: "Get one review-queue item by ID.",
|
|
330
|
+
inputSchema: { type: "object", properties: { dir: { type: "string" }, id: { type: "string" } }, required: ["id"] },
|
|
331
|
+
mutation: false,
|
|
332
|
+
async handler({ dir, id }) {
|
|
333
|
+
const item = loadQueue(resolveProjectRoot(dir)).find((i) => i.id === id);
|
|
334
|
+
if (!item) return denied(`No item with id ${id}`);
|
|
335
|
+
return { ok: true, item };
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
propose_fix: {
|
|
340
|
+
description: "Generate and gate-verify a fix proposal for a finding (does not touch the real working tree; saves a proposal awaiting approval). If id is omitted, targets the strongest open finding.",
|
|
341
|
+
inputSchema: { type: "object", properties: { dir: { type: "string" }, id: { type: "string" }, maxAttempts: { type: "number" } }, required: [] },
|
|
342
|
+
mutation: false,
|
|
343
|
+
async handler({ dir, id, maxAttempts }) {
|
|
344
|
+
const abs = resolveProjectRoot(dir);
|
|
345
|
+
const config = loadConfig(abs);
|
|
346
|
+
const targetId = id ?? chooseFixTarget(pending(loadQueue(abs)))?.id;
|
|
347
|
+
if (!targetId) return denied("No open check or modernize findings to target.");
|
|
348
|
+
const r = await runFixLifecycle(abs, targetId, config, { maxAttempts });
|
|
349
|
+
if (!r.ok) return denied(r.reason);
|
|
350
|
+
return { ok: true, findingId: targetId, status: r.proposalRecord.status, attempts: r.attempts, stopReason: r.stopReason, proposalPath: r.proposalPath };
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
verify_proposal: {
|
|
355
|
+
description: "Return the last recorded gate-verification results for a saved fix proposal.",
|
|
356
|
+
inputSchema: { type: "object", properties: { dir: { type: "string" }, id: { type: "string" } }, required: ["id"] },
|
|
357
|
+
mutation: false,
|
|
358
|
+
async handler({ dir, id }) {
|
|
359
|
+
const abs = resolveProjectRoot(dir);
|
|
360
|
+
const p = path.join(abs, ".mapd", "proposals", `${id}.json`);
|
|
361
|
+
if (!fs.existsSync(p)) return denied(`No saved proposal for finding ${id} — run propose_fix first.`);
|
|
362
|
+
const record = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
363
|
+
return { ok: true, status: record.status, attempts: record.attempts, stopReason: record.stopReason };
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
apply_approved_fix: {
|
|
368
|
+
description: "Apply a verified, awaiting-approval fix proposal to the real working tree. Requires approve:true from the calling agent — the one real mutation tool in this server.",
|
|
369
|
+
inputSchema: { type: "object", properties: { dir: { type: "string" }, id: { type: "string" }, approve: { type: "boolean" } }, required: ["id"] },
|
|
370
|
+
mutation: true,
|
|
371
|
+
async handler({ dir, id, approve }) {
|
|
372
|
+
const abs = resolveProjectRoot(dir);
|
|
373
|
+
if (approve !== true) {
|
|
374
|
+
return denied(
|
|
375
|
+
`apply_approved_fix requires an explicit approve:true argument — mutation was NOT applied.`,
|
|
376
|
+
{ requiresApproval: true, findingId: id },
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
const item = loadQueue(abs).find((i) => i.id === id && i.source === "fix");
|
|
380
|
+
if (!item) return denied(`No fix proposal with id ${id} in the review queue.`);
|
|
381
|
+
const config = loadConfig(abs);
|
|
382
|
+
const r = approveFixWithPostApplyVerification(abs, item, config);
|
|
383
|
+
recordAudit(abs, { command: "mcp:apply_approved_fix", initiator: "mcp", approvalStatus: r.ok ? "approved" : "failed", finalStatus: r.ok ? "applied" : "failed" });
|
|
384
|
+
if (!r.ok) return denied(r.detail);
|
|
385
|
+
return { ok: true, detail: r.detail, postApplyVerification: r.postApplyVerification };
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
generate_docs: {
|
|
390
|
+
description: "Render MAP.md from the current scored graph (mirrors `mapd docs`).",
|
|
391
|
+
inputSchema: { type: "object", properties: { dir: { type: "string" } }, required: [] },
|
|
392
|
+
mutation: false,
|
|
393
|
+
async handler({ dir }) {
|
|
394
|
+
const abs = resolveProjectRoot(dir);
|
|
395
|
+
const g = buildScoredGraph(abs);
|
|
396
|
+
const md = await renderDocs(g, { withNarration: false });
|
|
397
|
+
const out = path.join(abs, "MAP.md");
|
|
398
|
+
fs.writeFileSync(out, md);
|
|
399
|
+
return { ok: true, path: out };
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
|
|
403
|
+
get_mapd_status: {
|
|
404
|
+
description: "Return repo confidence, baseline status, open-findings count, and how many open findings come from stale reports.",
|
|
405
|
+
inputSchema: { type: "object", properties: { dir: { type: "string" } }, required: [] },
|
|
406
|
+
mutation: false,
|
|
407
|
+
async handler({ dir }) {
|
|
408
|
+
const abs = resolveProjectRoot(dir);
|
|
409
|
+
const g = buildScoredGraph(abs);
|
|
410
|
+
const loaded = loadBaseline(abs);
|
|
411
|
+
const { items } = loadQueueWithStates(abs);
|
|
412
|
+
const open = items.filter((i) => i.state === "active" || i.state === "stale");
|
|
413
|
+
return {
|
|
414
|
+
ok: true,
|
|
415
|
+
repoConfidence: g.repoConfidence,
|
|
416
|
+
baseline: loaded ? (loaded.schemaMismatch ? "schema-mismatch" : "present") : "none",
|
|
417
|
+
openFindings: open.length,
|
|
418
|
+
staleFindings: open.filter((i) => i.state === "stale").length,
|
|
419
|
+
};
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
}
|