openlore 2.0.7 → 2.0.8

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.
Files changed (90) hide show
  1. package/README.md +122 -20
  2. package/dist/cli/commands/mcp.d.ts +681 -0
  3. package/dist/cli/commands/mcp.d.ts.map +1 -1
  4. package/dist/cli/commands/mcp.js +384 -200
  5. package/dist/cli/commands/mcp.js.map +1 -1
  6. package/dist/constants.d.ts +6 -0
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/constants.js +14 -0
  9. package/dist/constants.js.map +1 -1
  10. package/dist/core/analyzer/artifact-generator.d.ts.map +1 -1
  11. package/dist/core/analyzer/artifact-generator.js +59 -4
  12. package/dist/core/analyzer/artifact-generator.js.map +1 -1
  13. package/dist/core/analyzer/call-graph.d.ts +19 -1
  14. package/dist/core/analyzer/call-graph.d.ts.map +1 -1
  15. package/dist/core/analyzer/call-graph.js +128 -28
  16. package/dist/core/analyzer/call-graph.js.map +1 -1
  17. package/dist/core/architecture/check.d.ts +63 -0
  18. package/dist/core/architecture/check.d.ts.map +1 -0
  19. package/dist/core/architecture/check.js +192 -0
  20. package/dist/core/architecture/check.js.map +1 -0
  21. package/dist/core/architecture/rules.d.ts +73 -0
  22. package/dist/core/architecture/rules.d.ts.map +1 -0
  23. package/dist/core/architecture/rules.js +201 -0
  24. package/dist/core/architecture/rules.js.map +1 -0
  25. package/dist/core/decisions/project.d.ts +59 -0
  26. package/dist/core/decisions/project.d.ts.map +1 -0
  27. package/dist/core/decisions/project.js +68 -0
  28. package/dist/core/decisions/project.js.map +1 -0
  29. package/dist/core/decisions/verifier.d.ts +10 -0
  30. package/dist/core/decisions/verifier.d.ts.map +1 -1
  31. package/dist/core/decisions/verifier.js +48 -5
  32. package/dist/core/decisions/verifier.js.map +1 -1
  33. package/dist/core/provenance/change-coupling.d.ts +68 -0
  34. package/dist/core/provenance/change-coupling.d.ts.map +1 -0
  35. package/dist/core/provenance/change-coupling.js +134 -0
  36. package/dist/core/provenance/change-coupling.js.map +1 -0
  37. package/dist/core/provenance/git-provenance.d.ts +67 -0
  38. package/dist/core/provenance/git-provenance.d.ts.map +1 -0
  39. package/dist/core/provenance/git-provenance.js +177 -0
  40. package/dist/core/provenance/git-provenance.js.map +1 -0
  41. package/dist/core/provenance/project.d.ts +37 -0
  42. package/dist/core/provenance/project.d.ts.map +1 -0
  43. package/dist/core/provenance/project.js +46 -0
  44. package/dist/core/provenance/project.js.map +1 -0
  45. package/dist/core/services/edge-store.d.ts +41 -0
  46. package/dist/core/services/edge-store.d.ts.map +1 -1
  47. package/dist/core/services/edge-store.js +251 -3
  48. package/dist/core/services/edge-store.js.map +1 -1
  49. package/dist/core/services/llm-service.d.ts +9 -0
  50. package/dist/core/services/llm-service.d.ts.map +1 -1
  51. package/dist/core/services/llm-service.js +15 -4
  52. package/dist/core/services/llm-service.js.map +1 -1
  53. package/dist/core/services/mcp-handlers/architecture.d.ts +19 -0
  54. package/dist/core/services/mcp-handlers/architecture.d.ts.map +1 -0
  55. package/dist/core/services/mcp-handlers/architecture.js +104 -0
  56. package/dist/core/services/mcp-handlers/architecture.js.map +1 -0
  57. package/dist/core/services/mcp-handlers/change-coupling.d.ts +16 -0
  58. package/dist/core/services/mcp-handlers/change-coupling.d.ts.map +1 -0
  59. package/dist/core/services/mcp-handlers/change-coupling.js +57 -0
  60. package/dist/core/services/mcp-handlers/change-coupling.js.map +1 -0
  61. package/dist/core/services/mcp-handlers/graph.d.ts +27 -0
  62. package/dist/core/services/mcp-handlers/graph.d.ts.map +1 -1
  63. package/dist/core/services/mcp-handlers/graph.js +98 -16
  64. package/dist/core/services/mcp-handlers/graph.js.map +1 -1
  65. package/dist/core/services/mcp-handlers/orient.d.ts.map +1 -1
  66. package/dist/core/services/mcp-handlers/orient.js +122 -2
  67. package/dist/core/services/mcp-handlers/orient.js.map +1 -1
  68. package/dist/core/services/mcp-handlers/reachability.d.ts +30 -0
  69. package/dist/core/services/mcp-handlers/reachability.d.ts.map +1 -0
  70. package/dist/core/services/mcp-handlers/reachability.js +222 -0
  71. package/dist/core/services/mcp-handlers/reachability.js.map +1 -0
  72. package/dist/core/services/mcp-handlers/structural-diff.d.ts +31 -0
  73. package/dist/core/services/mcp-handlers/structural-diff.d.ts.map +1 -0
  74. package/dist/core/services/mcp-handlers/structural-diff.js +268 -0
  75. package/dist/core/services/mcp-handlers/structural-diff.js.map +1 -0
  76. package/dist/core/services/mcp-handlers/test-impact.d.ts +34 -0
  77. package/dist/core/services/mcp-handlers/test-impact.d.ts.map +1 -0
  78. package/dist/core/services/mcp-handlers/test-impact.js +221 -0
  79. package/dist/core/services/mcp-handlers/test-impact.js.map +1 -0
  80. package/dist/core/services/mcp-handlers/tool-guard.d.ts +45 -0
  81. package/dist/core/services/mcp-handlers/tool-guard.d.ts.map +1 -0
  82. package/dist/core/services/mcp-handlers/tool-guard.js +81 -0
  83. package/dist/core/services/mcp-handlers/tool-guard.js.map +1 -0
  84. package/dist/core/services/mcp-handlers/utils.d.ts.map +1 -1
  85. package/dist/core/services/mcp-handlers/utils.js +15 -1
  86. package/dist/core/services/mcp-handlers/utils.js.map +1 -1
  87. package/dist/core/services/mcp-watcher.d.ts.map +1 -1
  88. package/dist/core/services/mcp-watcher.js +9 -0
  89. package/dist/core/services/mcp-watcher.js.map +1 -1
  90. package/package.json +8 -8
@@ -22,14 +22,20 @@ const _pkgVersion = _require('../../../package.json').version;
22
22
  import { Command } from 'commander';
23
23
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
24
24
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
25
- import { CallToolRequestSchema, InitializeRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
25
+ import { CallToolRequestSchema, InitializeRequestSchema, ListToolsRequestSchema, McpError, ErrorCode, LATEST_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS, } from '@modelcontextprotocol/sdk/types.js';
26
+ import { validateToolArgs, withToolTimeout, capOutput, classifyToolError, } from '../../core/services/mcp-handlers/tool-guard.js';
26
27
  import { sanitizeMcpError, validateDirectory } from '../../core/services/mcp-handlers/utils.js';
27
28
  import { createTracker, updateTracker, getFreshnessSignal } from '../../core/services/mcp-handlers/epistemic-lease.js';
28
29
  import { emit } from '../../core/services/telemetry.js';
29
- import { DEFAULT_DRIFT_MAX_FILES } from '../../constants.js';
30
+ import { DEFAULT_DRIFT_MAX_FILES, MCP_TOOL_MAX_BYTES } from '../../constants.js';
30
31
  import { handleGetCallGraph, handleGetSubgraph, handleAnalyzeImpact, handleGetLowRiskRefactorCandidates, handleGetLeafFunctions, handleGetCriticalHubs, handleGetGodFunctions, handleGetFileDependencies, handleTraceExecutionPath, } from '../../core/services/mcp-handlers/graph.js';
31
32
  import { handleSearchCode, handleSuggestInsertionPoints, handleSearchSpecs, handleListSpecDomains, handleGetSpec, handleUnifiedSearch, } from '../../core/services/mcp-handlers/semantic.js';
32
33
  import { handleOrient } from '../../core/services/mcp-handlers/orient.js';
34
+ import { handleSelectTests } from '../../core/services/mcp-handlers/test-impact.js';
35
+ import { handleFindDeadCode } from '../../core/services/mcp-handlers/reachability.js';
36
+ import { handleStructuralDiff } from '../../core/services/mcp-handlers/structural-diff.js';
37
+ import { handleGetChangeCoupling } from '../../core/services/mcp-handlers/change-coupling.js';
38
+ import { handleCheckArchitecture } from '../../core/services/mcp-handlers/architecture.js';
33
39
  import { handleGenerateChangeProposal, handleAnnotateStory } from '../../core/services/mcp-handlers/change.js';
34
40
  import { handleRecordDecision, handleListDecisions, handleApproveDecision, handleRejectDecision, handleSyncDecisions, } from '../../core/services/mcp-handlers/decisions.js';
35
41
  import { handleAnalyzeCodebase, handleGetArchitectureOverview, handleGetRefactorReport, handleGetDuplicateReport, handleGetSignatures, handleGetMapping, handleCheckSpecDrift, handleGetFunctionSkeleton, handleGetFunctionBody, handleGetDecisions, handleGetRouteInventory, handleGetMiddlewareInventory, handleGetSchemaInventory, handleGetUIComponents, handleGetEnvVars, handleGetExternalPackages, handleAuditSpecCoverage, handleGenerateTests, handleGetTestCoverage, handleGetMinimalContext, handleGetCluster, handleDetectChanges, } from '../../core/services/mcp-handlers/analysis.js';
@@ -339,6 +345,107 @@ export const TOOL_DEFINITIONS = [
339
345
  required: ['directory', 'symbol'],
340
346
  },
341
347
  },
348
+ {
349
+ name: 'select_tests',
350
+ description: 'USE THIS WHEN: you changed code and want to know which tests to run — ' +
351
+ '"which tests cover parseConfig?", "what should I run for this diff?". ' +
352
+ 'Walks the call graph BACKWARD from the change to every test that transitively reaches it, ' +
353
+ 'with the reaching path per test. Deterministic, offline, no test run. ' +
354
+ 'It is an over-approximate PRIORITIZER (run these first), not a sound replacement for the full ' +
355
+ 'suite — the response states its confidence and coverage. Run analyze_codebase first.',
356
+ inputSchema: {
357
+ type: 'object',
358
+ properties: {
359
+ directory: { type: 'string', description: 'Absolute path to the project directory' },
360
+ changedSymbols: {
361
+ type: 'array',
362
+ items: { type: 'string' },
363
+ description: 'Changed function/method names. Provide this OR diffRef.',
364
+ },
365
+ diffRef: {
366
+ type: 'string',
367
+ description: 'Git ref to diff the working tree against (e.g. "HEAD", "main"). Provide this OR changedSymbols.',
368
+ },
369
+ maxDepth: { type: 'number', description: 'Backward reachability depth (default 12)' },
370
+ },
371
+ required: ['directory'],
372
+ },
373
+ },
374
+ {
375
+ name: 'find_dead_code',
376
+ description: 'USE THIS WHEN: "what code is unreachable / dead?", "is anything calling X?", or ' +
377
+ '"what becomes dead if I delete X?". Cross-language mark-and-sweep reachability from roots ' +
378
+ '(tests, imported symbols, route handlers, main) over the call graph. ' +
379
+ 'Pass ifDeleted to get the downstream-only-reachable set for a symbol. ' +
380
+ 'Results are confidence-tagged CANDIDATES, never deletion authority — dynamic dispatch, DI, ' +
381
+ 'and external consumers cause false positives, stated in the response. Run analyze_codebase first.',
382
+ inputSchema: {
383
+ type: 'object',
384
+ properties: {
385
+ directory: { type: 'string', description: 'Absolute path to the project directory' },
386
+ ifDeleted: { type: 'string', description: 'Symbol name — returns what becomes dead if it is deleted (delete-impact mode)' },
387
+ maxResults: { type: 'number', description: 'Max candidate-dead results (default 100)' },
388
+ filePattern: { type: 'string', description: 'Only report candidates whose file path contains this substring' },
389
+ },
390
+ required: ['directory'],
391
+ },
392
+ },
393
+ {
394
+ name: 'structural_diff',
395
+ description: 'USE THIS WHEN reviewing or refactoring a change: "what changed structurally?", ' +
396
+ '"whose callers are now stale?". A graph diff (complement to git diff) between two states ' +
397
+ '(working tree vs a ref, or two refs): functions/edges added & removed, signature changes, ' +
398
+ 'and the existing callers now STALE because a callee signature moved under them. ' +
399
+ 'Rename/move ambiguity is flagged, not guessed. Deterministic, offline. Run analyze_codebase ' +
400
+ 'first for stale-caller analysis.',
401
+ inputSchema: {
402
+ type: 'object',
403
+ properties: {
404
+ directory: { type: 'string', description: 'Absolute path to the project directory' },
405
+ baseRef: { type: 'string', description: 'Old state to diff against (default "HEAD")' },
406
+ headRef: { type: 'string', description: 'New state (a git ref). Omit to use the working tree.' },
407
+ maxResults: { type: 'number', description: 'Cap reported items per category (default 200)' },
408
+ },
409
+ required: ['directory'],
410
+ },
411
+ },
412
+ {
413
+ name: 'get_change_coupling',
414
+ description: 'USE THIS WHEN: "what changes together with this file?" or "what is the most volatile code?". ' +
415
+ 'Mined from local git history (not the call graph): co-change coupling surfaces invisible ' +
416
+ 'coupling with no import/call edge (the config + parser that move in lockstep), and ' +
417
+ 'volatility/churn flags risky high-change code. Pass a file for its coupling, or omit for the ' +
418
+ 'most-volatile overview. Advisory signal (correlation, not causation); bulk commits filtered. ' +
419
+ 'Run analyze_codebase first.',
420
+ inputSchema: {
421
+ type: 'object',
422
+ properties: {
423
+ directory: { type: 'string', description: 'Absolute path to the project directory' },
424
+ file: { type: 'string', description: 'A file to query its coupling/volatility. Omit for the most-volatile overview.' },
425
+ limit: { type: 'number', description: 'Cap results (default 20)' },
426
+ },
427
+ required: ['directory'],
428
+ },
429
+ },
430
+ {
431
+ name: 'check_architecture',
432
+ description: 'USE THIS BEFORE adding an import to check it against the repo\'s architecture rules, or to ' +
433
+ 'list current architecture violations. Opt-in and inert unless the repo declares rules in ' +
434
+ '.openlore/architecture.json (layers / forbidden / allowedOnly) or via an "Invariant:" marker ' +
435
+ 'in a synced ADR. Pre-edit mode: pass {from, to} ("may a file under <from> import <to>?") for a ' +
436
+ 'deterministic allowed/denied + the governing rule + why, BEFORE you write the code. Scan mode: ' +
437
+ 'pass only {directory} for the full current-violations report. Cross-language, offline, ' +
438
+ 'deterministic; complements (does not replace) CI linters. Run analyze_codebase first.',
439
+ inputSchema: {
440
+ type: 'object',
441
+ properties: {
442
+ directory: { type: 'string', description: 'Absolute path to the project directory' },
443
+ from: { type: 'string', description: 'Pre-edit mode: the file that would gain the import (relative or absolute). Requires "to".' },
444
+ to: { type: 'string', description: 'Pre-edit mode: the target file path or exported symbol being imported. Requires "from".' },
445
+ },
446
+ required: ['directory'],
447
+ },
448
+ },
342
449
  {
343
450
  name: 'get_low_risk_refactor_candidates',
344
451
  description: 'Return the safest functions to refactor first: low fan-in (few callers), ' +
@@ -1116,7 +1223,7 @@ const TOOL_ANNOTATIONS = {
1116
1223
  orient: _RO, analyze_codebase: _RWI, get_architecture_overview: _RO,
1117
1224
  get_refactor_report: _RO, get_call_graph: _RO, get_duplicate_report: _RO,
1118
1225
  get_signatures: _RO, get_subgraph: _RO, trace_execution_path: _RO,
1119
- get_mapping: _RO, check_spec_drift: _RO, analyze_impact: _RO,
1226
+ get_mapping: _RO, check_spec_drift: _RO, analyze_impact: _RO, select_tests: _RO, find_dead_code: _RO, structural_diff: _RO, get_change_coupling: _RO, check_architecture: _RO,
1120
1227
  get_low_risk_refactor_candidates: _RO, get_leaf_functions: _RO,
1121
1228
  get_critical_hubs: _RO, get_function_skeleton: _RO, get_god_functions: _RO,
1122
1229
  suggest_insertion_points: _RO, search_code: _RO, list_spec_domains: _RO,
@@ -1129,6 +1236,21 @@ const TOOL_ANNOTATIONS = {
1129
1236
  detect_changes: _RO, record_decision: _RW, list_decisions: _RO,
1130
1237
  approve_decision: _RWI, reject_decision: _RWI, sync_decisions: _RWI,
1131
1238
  };
1239
+ // Tools that touch external entities (LLM / network) → openWorldHint: true.
1240
+ // Everything else is local, deterministic, closed-world analysis.
1241
+ const OPEN_WORLD_TOOLS = new Set(['generate_tests', 'generate_change_proposal', 'annotate_story']);
1242
+ /** Human-readable title from a snake_case tool name (spec-11 annotations). */
1243
+ function toolTitle(name) {
1244
+ return name.split('_').map(w => (w ? w[0].toUpperCase() + w.slice(1) : w)).join(' ');
1245
+ }
1246
+ /** Full MCP `annotations` for a tool: read/write hints + title + openWorldHint (spec-11). */
1247
+ export function toolAnnotations(name) {
1248
+ return {
1249
+ title: toolTitle(name),
1250
+ ...(TOOL_ANNOTATIONS[name] ?? _RO),
1251
+ openWorldHint: OPEN_WORLD_TOOLS.has(name),
1252
+ };
1253
+ }
1132
1254
  const MINIMAL_TOOLS = new Set([
1133
1255
  'orient', 'search_code', 'record_decision', 'detect_changes', 'check_spec_drift',
1134
1256
  ]);
@@ -1184,7 +1306,7 @@ async function startMcpServer(options = {}) {
1184
1306
  const { version: pkgVersion } = _require('../../../package.json');
1185
1307
  const server = new Server({ name: 'openlore', version: pkgVersion }, { capabilities: { tools: {} } });
1186
1308
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
1187
- tools: activeTools.map(t => ({ ...t, annotations: TOOL_ANNOTATIONS[t.name] })),
1309
+ tools: activeTools.map(t => ({ ...t, annotations: toolAnnotations(t.name) })),
1188
1310
  }));
1189
1311
  // Per-session epistemic lease tracker — re-initialized when directory changes.
1190
1312
  let tracker;
@@ -1197,8 +1319,16 @@ async function startMcpServer(options = {}) {
1197
1319
  server.setRequestHandler(InitializeRequestSchema, async (request) => {
1198
1320
  agentName = request.params.clientInfo?.name ?? 'unknown';
1199
1321
  agentVersion = request.params.clientInfo?.version ?? 'unknown';
1322
+ // Protocol negotiation (spec-12): echo the client's requested version when we
1323
+ // support it (per the SDK's pinned set), else offer our latest supported one.
1324
+ const requested = request.params.protocolVersion;
1325
+ const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(requested)
1326
+ ? requested
1327
+ : LATEST_PROTOCOL_VERSION;
1200
1328
  return {
1201
- protocolVersion: request.params.protocolVersion,
1329
+ protocolVersion,
1330
+ // Honest capabilities: only `tools`. No `listChanged` — the tool list is
1331
+ // static per session, so we don't advertise a capability we don't implement.
1202
1332
  capabilities: { tools: {} },
1203
1333
  serverInfo: { name: 'openlore', version: _pkgVersion },
1204
1334
  };
@@ -1225,6 +1355,17 @@ async function startMcpServer(options = {}) {
1225
1355
  const _dir = args.directory;
1226
1356
  const directory = typeof _dir === 'string' ? _dir : '';
1227
1357
  const _t0 = Date.now();
1358
+ // Input validation (spec-10) against the tool's own declared inputSchema, before
1359
+ // dispatch. Invalid args become a JSON-RPC -32602 error (spec-12), not an
1360
+ // isError tool result — a malformed request is a protocol error, not a tool failure.
1361
+ {
1362
+ const toolDef = TOOL_DEFINITIONS.find(t => t.name === name);
1363
+ const argError = toolDef ? validateToolArgs(args, toolDef.inputSchema) : null;
1364
+ if (argError) {
1365
+ emit(directory, 'mcp', { event: 'tool_error', tool: name, ms: Date.now() - _t0, agent: agentName, code: 'INVALID_ARGS', error: argError });
1366
+ throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for "${name}": ${argError}`);
1367
+ }
1368
+ }
1228
1369
  try {
1229
1370
  const filePath = args.filePath;
1230
1371
  // Init (or re-init when project directory changes between calls)
@@ -1236,205 +1377,240 @@ async function startMcpServer(options = {}) {
1236
1377
  if (tracker && directory)
1237
1378
  updateTracker(tracker, name, directory, typeof filePath === 'string' ? filePath : undefined);
1238
1379
  let result;
1239
- if (name === 'orient') {
1240
- const { task, limit = 5 } = args;
1241
- result = await handleOrient(directory, task, limit);
1242
- if (result && typeof result === 'object') {
1243
- const r = result;
1244
- emit(directory, 'orient', {
1245
- event: 'orient_call',
1246
- agent: agentName,
1247
- functions: Array.isArray(r['relevantFunctions']) ? r['relevantFunctions'].length : 0,
1248
- files: Array.isArray(r['relevantFiles']) ? r['relevantFiles'].length : 0,
1249
- spec_domains: Array.isArray(r['specDomains']) ? r['specDomains'].length : 0,
1250
- insertion_points: Array.isArray(r['insertionPoints']) ? r['insertionPoints'].length : 0,
1251
- });
1380
+ let _unknownTool = false;
1381
+ // Per-tool timeout (spec-10): race the dispatch against the tool's budget so a
1382
+ // pathological hang can never wedge the server. Slow tools (analysis, LLM) have
1383
+ // generous overrides in MCP_TOOL_TIMEOUT_OVERRIDES.
1384
+ await withToolTimeout((async () => {
1385
+ if (name === 'orient') {
1386
+ const { task, limit = 5 } = args;
1387
+ result = await handleOrient(directory, task, limit);
1388
+ if (result && typeof result === 'object') {
1389
+ const r = result;
1390
+ emit(directory, 'orient', {
1391
+ event: 'orient_call',
1392
+ agent: agentName,
1393
+ functions: Array.isArray(r['relevantFunctions']) ? r['relevantFunctions'].length : 0,
1394
+ files: Array.isArray(r['relevantFiles']) ? r['relevantFiles'].length : 0,
1395
+ spec_domains: Array.isArray(r['specDomains']) ? r['specDomains'].length : 0,
1396
+ insertion_points: Array.isArray(r['insertionPoints']) ? r['insertionPoints'].length : 0,
1397
+ });
1398
+ }
1252
1399
  }
1253
- }
1254
- else if (name === 'analyze_codebase') {
1255
- const { directory, force = false } = args;
1256
- result = await handleAnalyzeCodebase(directory, force);
1257
- }
1258
- else if (name === 'get_architecture_overview') {
1259
- const { directory } = args;
1260
- result = await handleGetArchitectureOverview(directory);
1261
- }
1262
- else if (name === 'get_refactor_report') {
1263
- const { directory } = args;
1264
- result = await handleGetRefactorReport(directory);
1265
- }
1266
- else if (name === 'get_call_graph') {
1267
- const { directory } = args;
1268
- result = await handleGetCallGraph(directory);
1269
- }
1270
- else if (name === 'get_signatures') {
1271
- const { directory, filePattern } = args;
1272
- result = await handleGetSignatures(directory, filePattern);
1273
- }
1274
- else if (name === 'get_subgraph') {
1275
- const { directory, functionName, direction = 'downstream', maxDepth = 3, format = 'json' } = args;
1276
- result = await handleGetSubgraph(directory, functionName, direction, maxDepth, format);
1277
- }
1278
- else if (name === 'trace_execution_path') {
1279
- const { directory, entryFunction, targetFunction, maxDepth = 6, maxPaths = 10 } = args;
1280
- result = await handleTraceExecutionPath(directory, entryFunction, targetFunction, maxDepth, maxPaths);
1281
- }
1282
- else if (name === 'get_mapping') {
1283
- const { directory, domain, orphansOnly } = args;
1284
- result = await handleGetMapping(directory, domain, orphansOnly);
1285
- }
1286
- else if (name === 'analyze_impact') {
1287
- const { directory, symbol, depth = 2 } = args;
1288
- result = await handleAnalyzeImpact(directory, symbol, depth);
1289
- }
1290
- else if (name === 'get_low_risk_refactor_candidates') {
1291
- const { directory, limit = 5, filePattern } = args;
1292
- result = await handleGetLowRiskRefactorCandidates(directory, limit, filePattern);
1293
- }
1294
- else if (name === 'get_leaf_functions') {
1295
- const { directory, limit = 20, filePattern, sortBy = 'fanIn' } = args;
1296
- result = await handleGetLeafFunctions(directory, limit, filePattern, sortBy);
1297
- }
1298
- else if (name === 'get_critical_hubs') {
1299
- const { directory, limit = 10, minFanIn = 3 } = args;
1300
- result = await handleGetCriticalHubs(directory, limit, minFanIn);
1301
- }
1302
- else if (name === 'get_duplicate_report') {
1303
- const { directory } = args;
1304
- result = await handleGetDuplicateReport(directory);
1305
- }
1306
- else if (name === 'get_function_skeleton') {
1307
- const { directory, filePath } = args;
1308
- result = await handleGetFunctionSkeleton(directory, filePath);
1309
- }
1310
- else if (name === 'get_god_functions') {
1311
- const { directory, filePath, fanOutThreshold = 8 } = args;
1312
- result = await handleGetGodFunctions(directory, filePath, fanOutThreshold);
1313
- }
1314
- else if (name === 'check_spec_drift') {
1315
- const { directory, base = 'auto', files = [], domains = [], failOn = 'warning', maxFiles = DEFAULT_DRIFT_MAX_FILES } = args;
1316
- result = await handleCheckSpecDrift(directory, base, files, domains, failOn, maxFiles);
1317
- }
1318
- else if (name === 'search_code') {
1319
- const { directory, query, limit = 10, language, minFanIn } = args;
1320
- result = await handleSearchCode(directory, query, limit, language, minFanIn);
1321
- }
1322
- else if (name === 'suggest_insertion_points') {
1323
- const { directory, description, limit = 5, language } = args;
1324
- result = await handleSuggestInsertionPoints(directory, description, limit, language);
1325
- }
1326
- else if (name === 'search_specs') {
1327
- const { directory, query, limit = 10, domain, section } = args;
1328
- result = await handleSearchSpecs(directory, query, limit, domain, section);
1329
- }
1330
- else if (name === 'search_unified') {
1331
- const { directory, query, limit = 10, language, domain, section } = args;
1332
- result = await handleUnifiedSearch(directory, query, limit, language, domain, section);
1333
- }
1334
- else if (name === 'list_spec_domains') {
1335
- const { directory } = args;
1336
- result = await handleListSpecDomains(directory);
1337
- }
1338
- else if (name === 'get_spec') {
1339
- const { directory, domain } = args;
1340
- result = await handleGetSpec(directory, domain);
1341
- }
1342
- else if (name === 'get_function_body') {
1343
- const { directory, filePath, functionName } = args;
1344
- result = await handleGetFunctionBody(directory, filePath, functionName);
1345
- }
1346
- else if (name === 'get_file_dependencies') {
1347
- const { directory, filePath, direction = 'both' } = args;
1348
- result = await handleGetFileDependencies(directory, filePath, direction);
1349
- }
1350
- else if (name === 'generate_change_proposal') {
1351
- const { directory, description, slug, storyContent } = args;
1352
- result = await handleGenerateChangeProposal(directory, description, slug, storyContent);
1353
- }
1354
- else if (name === 'annotate_story') {
1355
- const { directory, storyFilePath, description } = args;
1356
- result = await handleAnnotateStory(directory, storyFilePath, description);
1357
- }
1358
- else if (name === 'get_decisions') {
1359
- const { directory, query } = args;
1360
- result = await handleGetDecisions(directory, query);
1361
- }
1362
- else if (name === 'get_route_inventory') {
1363
- const { directory } = args;
1364
- result = await handleGetRouteInventory(directory);
1365
- }
1366
- else if (name === 'get_middleware_inventory') {
1367
- const { directory } = args;
1368
- result = await handleGetMiddlewareInventory(directory);
1369
- }
1370
- else if (name === 'get_schema_inventory') {
1371
- const { directory } = args;
1372
- result = await handleGetSchemaInventory(directory);
1373
- }
1374
- else if (name === 'get_ui_components') {
1375
- const { directory } = args;
1376
- result = await handleGetUIComponents(directory);
1377
- }
1378
- else if (name === 'get_env_vars') {
1379
- const { directory } = args;
1380
- result = await handleGetEnvVars(directory);
1381
- }
1382
- else if (name === 'get_external_packages') {
1383
- const { directory } = args;
1384
- result = await handleGetExternalPackages(directory);
1385
- }
1386
- else if (name === 'audit_spec_coverage') {
1387
- const { directory, maxUncovered = 50, hubThreshold = 5 } = args;
1388
- result = await handleAuditSpecCoverage(directory, maxUncovered, hubThreshold);
1389
- }
1390
- else if (name === 'generate_tests') {
1391
- const { directory, domains, framework, useLlm, dryRun } = args;
1392
- result = await handleGenerateTests({ directory, domains, framework, useLlm, dryRun });
1393
- }
1394
- else if (name === 'get_test_coverage') {
1395
- const { directory, domains, minCoverage } = args;
1396
- result = await handleGetTestCoverage({ directory, domains, minCoverage });
1397
- }
1398
- else if (name === 'get_minimal_context') {
1399
- const { directory, functionName, filePath } = args;
1400
- result = await handleGetMinimalContext(directory, functionName, filePath);
1401
- }
1402
- else if (name === 'get_cluster') {
1403
- const { directory, functionName } = args;
1404
- result = await handleGetCluster(directory, functionName);
1405
- }
1406
- else if (name === 'detect_changes') {
1407
- const { directory, base } = args;
1408
- result = await handleDetectChanges(directory, base);
1409
- }
1410
- else if (name === 'record_decision') {
1411
- const { directory, title, rationale, consequences, affectedFiles, supersedes, scope } = args;
1412
- result = await handleRecordDecision(directory, title, rationale, consequences, affectedFiles, supersedes, scope);
1413
- }
1414
- else if (name === 'list_decisions') {
1415
- const { directory, status } = args;
1416
- result = await handleListDecisions(directory, status);
1417
- }
1418
- else if (name === 'approve_decision') {
1419
- const { directory, id, note } = args;
1420
- result = await handleApproveDecision(directory, id, note);
1421
- }
1422
- else if (name === 'reject_decision') {
1423
- const { directory, id, note } = args;
1424
- result = await handleRejectDecision(directory, id, note);
1425
- }
1426
- else if (name === 'sync_decisions') {
1427
- const { directory, dryRun = false, id } = args;
1428
- result = await handleSyncDecisions(directory, dryRun, id);
1429
- }
1430
- else {
1400
+ else if (name === 'analyze_codebase') {
1401
+ const { directory, force = false } = args;
1402
+ result = await handleAnalyzeCodebase(directory, force);
1403
+ }
1404
+ else if (name === 'get_architecture_overview') {
1405
+ const { directory } = args;
1406
+ result = await handleGetArchitectureOverview(directory);
1407
+ }
1408
+ else if (name === 'get_refactor_report') {
1409
+ const { directory } = args;
1410
+ result = await handleGetRefactorReport(directory);
1411
+ }
1412
+ else if (name === 'get_call_graph') {
1413
+ const { directory } = args;
1414
+ result = await handleGetCallGraph(directory);
1415
+ }
1416
+ else if (name === 'get_signatures') {
1417
+ const { directory, filePattern } = args;
1418
+ result = await handleGetSignatures(directory, filePattern);
1419
+ }
1420
+ else if (name === 'get_subgraph') {
1421
+ const { directory, functionName, direction = 'downstream', maxDepth = 3, format = 'json' } = args;
1422
+ result = await handleGetSubgraph(directory, functionName, direction, maxDepth, format);
1423
+ }
1424
+ else if (name === 'trace_execution_path') {
1425
+ const { directory, entryFunction, targetFunction, maxDepth = 6, maxPaths = 10 } = args;
1426
+ result = await handleTraceExecutionPath(directory, entryFunction, targetFunction, maxDepth, maxPaths);
1427
+ }
1428
+ else if (name === 'get_mapping') {
1429
+ const { directory, domain, orphansOnly } = args;
1430
+ result = await handleGetMapping(directory, domain, orphansOnly);
1431
+ }
1432
+ else if (name === 'analyze_impact') {
1433
+ const { directory, symbol, depth = 2 } = args;
1434
+ result = await handleAnalyzeImpact(directory, symbol, depth);
1435
+ }
1436
+ else if (name === 'select_tests') {
1437
+ const { directory, changedSymbols, diffRef, maxDepth } = args;
1438
+ result = await handleSelectTests({ directory, changedSymbols, diffRef, maxDepth });
1439
+ }
1440
+ else if (name === 'find_dead_code') {
1441
+ const { directory, ifDeleted, maxResults, filePattern } = args;
1442
+ result = await handleFindDeadCode({ directory, ifDeleted, maxResults, filePattern });
1443
+ }
1444
+ else if (name === 'structural_diff') {
1445
+ const { directory, baseRef, headRef, maxResults } = args;
1446
+ result = await handleStructuralDiff({ directory, baseRef, headRef, maxResults });
1447
+ }
1448
+ else if (name === 'get_change_coupling') {
1449
+ const { directory, file, limit } = args;
1450
+ result = await handleGetChangeCoupling({ directory, file, limit });
1451
+ }
1452
+ else if (name === 'check_architecture') {
1453
+ const { directory, from, to } = args;
1454
+ result = await handleCheckArchitecture({ directory, from, to });
1455
+ }
1456
+ else if (name === 'get_low_risk_refactor_candidates') {
1457
+ const { directory, limit = 5, filePattern } = args;
1458
+ result = await handleGetLowRiskRefactorCandidates(directory, limit, filePattern);
1459
+ }
1460
+ else if (name === 'get_leaf_functions') {
1461
+ const { directory, limit = 20, filePattern, sortBy = 'fanIn' } = args;
1462
+ result = await handleGetLeafFunctions(directory, limit, filePattern, sortBy);
1463
+ }
1464
+ else if (name === 'get_critical_hubs') {
1465
+ const { directory, limit = 10, minFanIn = 3 } = args;
1466
+ result = await handleGetCriticalHubs(directory, limit, minFanIn);
1467
+ }
1468
+ else if (name === 'get_duplicate_report') {
1469
+ const { directory } = args;
1470
+ result = await handleGetDuplicateReport(directory);
1471
+ }
1472
+ else if (name === 'get_function_skeleton') {
1473
+ const { directory, filePath } = args;
1474
+ result = await handleGetFunctionSkeleton(directory, filePath);
1475
+ }
1476
+ else if (name === 'get_god_functions') {
1477
+ const { directory, filePath, fanOutThreshold = 8 } = args;
1478
+ result = await handleGetGodFunctions(directory, filePath, fanOutThreshold);
1479
+ }
1480
+ else if (name === 'check_spec_drift') {
1481
+ const { directory, base = 'auto', files = [], domains = [], failOn = 'warning', maxFiles = DEFAULT_DRIFT_MAX_FILES } = args;
1482
+ result = await handleCheckSpecDrift(directory, base, files, domains, failOn, maxFiles);
1483
+ }
1484
+ else if (name === 'search_code') {
1485
+ const { directory, query, limit = 10, language, minFanIn } = args;
1486
+ result = await handleSearchCode(directory, query, limit, language, minFanIn);
1487
+ }
1488
+ else if (name === 'suggest_insertion_points') {
1489
+ const { directory, description, limit = 5, language } = args;
1490
+ result = await handleSuggestInsertionPoints(directory, description, limit, language);
1491
+ }
1492
+ else if (name === 'search_specs') {
1493
+ const { directory, query, limit = 10, domain, section } = args;
1494
+ result = await handleSearchSpecs(directory, query, limit, domain, section);
1495
+ }
1496
+ else if (name === 'search_unified') {
1497
+ const { directory, query, limit = 10, language, domain, section } = args;
1498
+ result = await handleUnifiedSearch(directory, query, limit, language, domain, section);
1499
+ }
1500
+ else if (name === 'list_spec_domains') {
1501
+ const { directory } = args;
1502
+ result = await handleListSpecDomains(directory);
1503
+ }
1504
+ else if (name === 'get_spec') {
1505
+ const { directory, domain } = args;
1506
+ result = await handleGetSpec(directory, domain);
1507
+ }
1508
+ else if (name === 'get_function_body') {
1509
+ const { directory, filePath, functionName } = args;
1510
+ result = await handleGetFunctionBody(directory, filePath, functionName);
1511
+ }
1512
+ else if (name === 'get_file_dependencies') {
1513
+ const { directory, filePath, direction = 'both' } = args;
1514
+ result = await handleGetFileDependencies(directory, filePath, direction);
1515
+ }
1516
+ else if (name === 'generate_change_proposal') {
1517
+ const { directory, description, slug, storyContent } = args;
1518
+ result = await handleGenerateChangeProposal(directory, description, slug, storyContent);
1519
+ }
1520
+ else if (name === 'annotate_story') {
1521
+ const { directory, storyFilePath, description } = args;
1522
+ result = await handleAnnotateStory(directory, storyFilePath, description);
1523
+ }
1524
+ else if (name === 'get_decisions') {
1525
+ const { directory, query } = args;
1526
+ result = await handleGetDecisions(directory, query);
1527
+ }
1528
+ else if (name === 'get_route_inventory') {
1529
+ const { directory } = args;
1530
+ result = await handleGetRouteInventory(directory);
1531
+ }
1532
+ else if (name === 'get_middleware_inventory') {
1533
+ const { directory } = args;
1534
+ result = await handleGetMiddlewareInventory(directory);
1535
+ }
1536
+ else if (name === 'get_schema_inventory') {
1537
+ const { directory } = args;
1538
+ result = await handleGetSchemaInventory(directory);
1539
+ }
1540
+ else if (name === 'get_ui_components') {
1541
+ const { directory } = args;
1542
+ result = await handleGetUIComponents(directory);
1543
+ }
1544
+ else if (name === 'get_env_vars') {
1545
+ const { directory } = args;
1546
+ result = await handleGetEnvVars(directory);
1547
+ }
1548
+ else if (name === 'get_external_packages') {
1549
+ const { directory } = args;
1550
+ result = await handleGetExternalPackages(directory);
1551
+ }
1552
+ else if (name === 'audit_spec_coverage') {
1553
+ const { directory, maxUncovered = 50, hubThreshold = 5 } = args;
1554
+ result = await handleAuditSpecCoverage(directory, maxUncovered, hubThreshold);
1555
+ }
1556
+ else if (name === 'generate_tests') {
1557
+ const { directory, domains, framework, useLlm, dryRun } = args;
1558
+ result = await handleGenerateTests({ directory, domains, framework, useLlm, dryRun });
1559
+ }
1560
+ else if (name === 'get_test_coverage') {
1561
+ const { directory, domains, minCoverage } = args;
1562
+ result = await handleGetTestCoverage({ directory, domains, minCoverage });
1563
+ }
1564
+ else if (name === 'get_minimal_context') {
1565
+ const { directory, functionName, filePath } = args;
1566
+ result = await handleGetMinimalContext(directory, functionName, filePath);
1567
+ }
1568
+ else if (name === 'get_cluster') {
1569
+ const { directory, functionName } = args;
1570
+ result = await handleGetCluster(directory, functionName);
1571
+ }
1572
+ else if (name === 'detect_changes') {
1573
+ const { directory, base } = args;
1574
+ result = await handleDetectChanges(directory, base);
1575
+ }
1576
+ else if (name === 'record_decision') {
1577
+ const { directory, title, rationale, consequences, affectedFiles, supersedes, scope } = args;
1578
+ result = await handleRecordDecision(directory, title, rationale, consequences, affectedFiles, supersedes, scope);
1579
+ }
1580
+ else if (name === 'list_decisions') {
1581
+ const { directory, status } = args;
1582
+ result = await handleListDecisions(directory, status);
1583
+ }
1584
+ else if (name === 'approve_decision') {
1585
+ const { directory, id, note } = args;
1586
+ result = await handleApproveDecision(directory, id, note);
1587
+ }
1588
+ else if (name === 'reject_decision') {
1589
+ const { directory, id, note } = args;
1590
+ result = await handleRejectDecision(directory, id, note);
1591
+ }
1592
+ else if (name === 'sync_decisions') {
1593
+ const { directory, dryRun = false, id } = args;
1594
+ result = await handleSyncDecisions(directory, dryRun, id);
1595
+ }
1596
+ else {
1597
+ _unknownTool = true;
1598
+ }
1599
+ })(), name);
1600
+ if (_unknownTool) {
1431
1601
  return {
1432
1602
  content: [{ type: 'text', text: `Unknown tool: ${name}` }],
1433
1603
  isError: true,
1434
1604
  };
1435
1605
  }
1436
- emit(directory, 'mcp', { event: 'tool_call', tool: name, ms: Date.now() - _t0, agent: agentName, agent_version: agentVersion });
1437
- const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
1606
+ const rawText = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
1607
+ // Output cap (spec-10): truncate deterministically with a how-to-narrow note
1608
+ // rather than silently dropping data or blowing the agent's context.
1609
+ const { text, truncated } = capOutput(rawText, MCP_TOOL_MAX_BYTES);
1610
+ emit(directory, 'mcp', {
1611
+ event: 'tool_call', tool: name, ms: Date.now() - _t0, agent: agentName, agent_version: agentVersion,
1612
+ bytes: Buffer.byteLength(text, 'utf8'), outcome: truncated ? 'truncated' : 'ok',
1613
+ });
1438
1614
  const signal = tracker ? getFreshnessSignal(tracker) : null;
1439
1615
  // Freshness signal is a separate content item — never concatenated into
1440
1616
  // the result body — so structured outputs (JSON, patches) are not corrupted.
@@ -1446,9 +1622,17 @@ async function startMcpServer(options = {}) {
1446
1622
  return { content };
1447
1623
  }
1448
1624
  catch (err) {
1449
- emit(directory, 'mcp', { event: 'tool_error', tool: name, ms: Date.now() - _t0, agent: agentName, error: sanitizeMcpError(err) });
1625
+ // A thrown McpError is a protocol-level error (e.g. -32602) let the SDK
1626
+ // serialize it as a JSON-RPC error response, not a tool isError result.
1627
+ if (err instanceof McpError)
1628
+ throw err;
1629
+ // Error normalization (spec-10): a stable code taxonomy, distinguishing
1630
+ // "repo not analyzed yet" (actionable) from real failures and timeouts.
1631
+ const code = classifyToolError(err);
1632
+ const message = sanitizeMcpError(err);
1633
+ emit(directory, 'mcp', { event: 'tool_error', tool: name, ms: Date.now() - _t0, agent: agentName, code, outcome: 'error', error: message });
1450
1634
  return {
1451
- content: [{ type: 'text', text: `Tool error: ${sanitizeMcpError(err)}` }],
1635
+ content: [{ type: 'text', text: `Tool error [${code}]: ${message}` }],
1452
1636
  isError: true,
1453
1637
  };
1454
1638
  }