openlore 2.0.7 → 2.0.9

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 (162) hide show
  1. package/README.md +160 -25
  2. package/dist/api/init.d.ts.map +1 -1
  3. package/dist/api/init.js +9 -2
  4. package/dist/api/init.js.map +1 -1
  5. package/dist/cli/commands/analyze.d.ts.map +1 -1
  6. package/dist/cli/commands/analyze.js +5 -3
  7. package/dist/cli/commands/analyze.js.map +1 -1
  8. package/dist/cli/commands/decisions.d.ts +0 -1
  9. package/dist/cli/commands/decisions.d.ts.map +1 -1
  10. package/dist/cli/commands/decisions.js +19 -31
  11. package/dist/cli/commands/decisions.js.map +1 -1
  12. package/dist/cli/commands/doctor.d.ts.map +1 -1
  13. package/dist/cli/commands/doctor.js +85 -23
  14. package/dist/cli/commands/doctor.js.map +1 -1
  15. package/dist/cli/commands/init.d.ts.map +1 -1
  16. package/dist/cli/commands/init.js +16 -3
  17. package/dist/cli/commands/init.js.map +1 -1
  18. package/dist/cli/commands/mcp.d.ts +766 -0
  19. package/dist/cli/commands/mcp.d.ts.map +1 -1
  20. package/dist/cli/commands/mcp.js +444 -244
  21. package/dist/cli/commands/mcp.js.map +1 -1
  22. package/dist/cli/commands/orient.d.ts.map +1 -1
  23. package/dist/cli/commands/orient.js +14 -2
  24. package/dist/cli/commands/orient.js.map +1 -1
  25. package/dist/cli/commands/prove.d.ts +29 -0
  26. package/dist/cli/commands/prove.d.ts.map +1 -0
  27. package/dist/cli/commands/prove.js +160 -0
  28. package/dist/cli/commands/prove.js.map +1 -0
  29. package/dist/cli/commands/setup.d.ts.map +1 -1
  30. package/dist/cli/commands/setup.js +5 -3
  31. package/dist/cli/commands/setup.js.map +1 -1
  32. package/dist/cli/index.js +2 -0
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/cli/install/adapters/claude-code.d.ts +8 -2
  35. package/dist/cli/install/adapters/claude-code.d.ts.map +1 -1
  36. package/dist/cli/install/adapters/claude-code.js +99 -35
  37. package/dist/cli/install/adapters/claude-code.js.map +1 -1
  38. package/dist/cli/install/detect.d.ts.map +1 -1
  39. package/dist/cli/install/detect.js +14 -0
  40. package/dist/cli/install/detect.js.map +1 -1
  41. package/dist/cli/install/templates/agent-instructions.md +4 -2
  42. package/dist/constants.d.ts +16 -3
  43. package/dist/constants.d.ts.map +1 -1
  44. package/dist/constants.js +24 -3
  45. package/dist/constants.js.map +1 -1
  46. package/dist/core/agent-eval/measure.d.ts +57 -0
  47. package/dist/core/agent-eval/measure.d.ts.map +1 -0
  48. package/dist/core/agent-eval/measure.js +85 -0
  49. package/dist/core/agent-eval/measure.js.map +1 -0
  50. package/dist/core/agent-eval/scorecard.d.ts +37 -0
  51. package/dist/core/agent-eval/scorecard.d.ts.map +1 -0
  52. package/dist/core/agent-eval/scorecard.js +70 -0
  53. package/dist/core/agent-eval/scorecard.js.map +1 -0
  54. package/dist/core/agent-eval/tasks.d.ts +37 -0
  55. package/dist/core/agent-eval/tasks.d.ts.map +1 -0
  56. package/dist/core/agent-eval/tasks.js +76 -0
  57. package/dist/core/agent-eval/tasks.js.map +1 -0
  58. package/dist/core/analyzer/architecture-writer.d.ts +5 -3
  59. package/dist/core/analyzer/architecture-writer.d.ts.map +1 -1
  60. package/dist/core/analyzer/architecture-writer.js +6 -4
  61. package/dist/core/analyzer/architecture-writer.js.map +1 -1
  62. package/dist/core/analyzer/artifact-generator.d.ts.map +1 -1
  63. package/dist/core/analyzer/artifact-generator.js +59 -4
  64. package/dist/core/analyzer/artifact-generator.js.map +1 -1
  65. package/dist/core/analyzer/call-graph.d.ts +19 -1
  66. package/dist/core/analyzer/call-graph.d.ts.map +1 -1
  67. package/dist/core/analyzer/call-graph.js +130 -28
  68. package/dist/core/analyzer/call-graph.js.map +1 -1
  69. package/dist/core/analyzer/spec-vector-index.d.ts.map +1 -1
  70. package/dist/core/analyzer/spec-vector-index.js +6 -2
  71. package/dist/core/analyzer/spec-vector-index.js.map +1 -1
  72. package/dist/core/architecture/check.d.ts +63 -0
  73. package/dist/core/architecture/check.d.ts.map +1 -0
  74. package/dist/core/architecture/check.js +192 -0
  75. package/dist/core/architecture/check.js.map +1 -0
  76. package/dist/core/architecture/rules.d.ts +73 -0
  77. package/dist/core/architecture/rules.d.ts.map +1 -0
  78. package/dist/core/architecture/rules.js +201 -0
  79. package/dist/core/architecture/rules.js.map +1 -0
  80. package/dist/core/decisions/project.d.ts +59 -0
  81. package/dist/core/decisions/project.d.ts.map +1 -0
  82. package/dist/core/decisions/project.js +68 -0
  83. package/dist/core/decisions/project.js.map +1 -0
  84. package/dist/core/decisions/verifier.d.ts +10 -0
  85. package/dist/core/decisions/verifier.d.ts.map +1 -1
  86. package/dist/core/decisions/verifier.js +48 -5
  87. package/dist/core/decisions/verifier.js.map +1 -1
  88. package/dist/core/provenance/change-coupling.d.ts +68 -0
  89. package/dist/core/provenance/change-coupling.d.ts.map +1 -0
  90. package/dist/core/provenance/change-coupling.js +134 -0
  91. package/dist/core/provenance/change-coupling.js.map +1 -0
  92. package/dist/core/provenance/git-provenance.d.ts +67 -0
  93. package/dist/core/provenance/git-provenance.d.ts.map +1 -0
  94. package/dist/core/provenance/git-provenance.js +177 -0
  95. package/dist/core/provenance/git-provenance.js.map +1 -0
  96. package/dist/core/provenance/project.d.ts +37 -0
  97. package/dist/core/provenance/project.d.ts.map +1 -0
  98. package/dist/core/provenance/project.js +46 -0
  99. package/dist/core/provenance/project.js.map +1 -0
  100. package/dist/core/services/config-manager.d.ts +17 -0
  101. package/dist/core/services/config-manager.d.ts.map +1 -1
  102. package/dist/core/services/config-manager.js +35 -1
  103. package/dist/core/services/config-manager.js.map +1 -1
  104. package/dist/core/services/edge-store.d.ts +41 -0
  105. package/dist/core/services/edge-store.d.ts.map +1 -1
  106. package/dist/core/services/edge-store.js +251 -3
  107. package/dist/core/services/edge-store.js.map +1 -1
  108. package/dist/core/services/llm-service.d.ts +9 -0
  109. package/dist/core/services/llm-service.d.ts.map +1 -1
  110. package/dist/core/services/llm-service.js +15 -4
  111. package/dist/core/services/llm-service.js.map +1 -1
  112. package/dist/core/services/mcp-handlers/architecture.d.ts +19 -0
  113. package/dist/core/services/mcp-handlers/architecture.d.ts.map +1 -0
  114. package/dist/core/services/mcp-handlers/architecture.js +104 -0
  115. package/dist/core/services/mcp-handlers/architecture.js.map +1 -0
  116. package/dist/core/services/mcp-handlers/change-coupling.d.ts +16 -0
  117. package/dist/core/services/mcp-handlers/change-coupling.d.ts.map +1 -0
  118. package/dist/core/services/mcp-handlers/change-coupling.js +57 -0
  119. package/dist/core/services/mcp-handlers/change-coupling.js.map +1 -0
  120. package/dist/core/services/mcp-handlers/graph.d.ts +27 -0
  121. package/dist/core/services/mcp-handlers/graph.d.ts.map +1 -1
  122. package/dist/core/services/mcp-handlers/graph.js +98 -16
  123. package/dist/core/services/mcp-handlers/graph.js.map +1 -1
  124. package/dist/core/services/mcp-handlers/orient.d.ts +1 -1
  125. package/dist/core/services/mcp-handlers/orient.d.ts.map +1 -1
  126. package/dist/core/services/mcp-handlers/orient.js +196 -39
  127. package/dist/core/services/mcp-handlers/orient.js.map +1 -1
  128. package/dist/core/services/mcp-handlers/progressive.d.ts +46 -0
  129. package/dist/core/services/mcp-handlers/progressive.d.ts.map +1 -0
  130. package/dist/core/services/mcp-handlers/progressive.js +0 -0
  131. package/dist/core/services/mcp-handlers/progressive.js.map +1 -0
  132. package/dist/core/services/mcp-handlers/reachability.d.ts +30 -0
  133. package/dist/core/services/mcp-handlers/reachability.d.ts.map +1 -0
  134. package/dist/core/services/mcp-handlers/reachability.js +222 -0
  135. package/dist/core/services/mcp-handlers/reachability.js.map +1 -0
  136. package/dist/core/services/mcp-handlers/semantic.d.ts +1 -1
  137. package/dist/core/services/mcp-handlers/semantic.d.ts.map +1 -1
  138. package/dist/core/services/mcp-handlers/semantic.js +39 -26
  139. package/dist/core/services/mcp-handlers/semantic.js.map +1 -1
  140. package/dist/core/services/mcp-handlers/structural-diff.d.ts +31 -0
  141. package/dist/core/services/mcp-handlers/structural-diff.d.ts.map +1 -0
  142. package/dist/core/services/mcp-handlers/structural-diff.js +268 -0
  143. package/dist/core/services/mcp-handlers/structural-diff.js.map +1 -0
  144. package/dist/core/services/mcp-handlers/test-impact.d.ts +34 -0
  145. package/dist/core/services/mcp-handlers/test-impact.d.ts.map +1 -0
  146. package/dist/core/services/mcp-handlers/test-impact.js +221 -0
  147. package/dist/core/services/mcp-handlers/test-impact.js.map +1 -0
  148. package/dist/core/services/mcp-handlers/tool-guard.d.ts +45 -0
  149. package/dist/core/services/mcp-handlers/tool-guard.d.ts.map +1 -0
  150. package/dist/core/services/mcp-handlers/tool-guard.js +81 -0
  151. package/dist/core/services/mcp-handlers/tool-guard.js.map +1 -0
  152. package/dist/core/services/mcp-handlers/utils.d.ts.map +1 -1
  153. package/dist/core/services/mcp-handlers/utils.js +15 -1
  154. package/dist/core/services/mcp-handlers/utils.js.map +1 -1
  155. package/dist/core/services/mcp-watcher.d.ts +7 -0
  156. package/dist/core/services/mcp-watcher.d.ts.map +1 -1
  157. package/dist/core/services/mcp-watcher.js +46 -0
  158. package/dist/core/services/mcp-watcher.js.map +1 -1
  159. package/dist/core/services/project-detector.d.ts.map +1 -1
  160. package/dist/core/services/project-detector.js +45 -2
  161. package/dist/core/services/project-detector.js.map +1 -1
  162. 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';
@@ -40,6 +46,11 @@ export { handleGetCallGraph, handleGetSubgraph, handleAnalyzeImpact, handleGetLo
40
46
  // ============================================================================
41
47
  // TOOL DEFINITIONS
42
48
  // ============================================================================
49
+ // Spec 28 — the `directory` param is the one input every tool shares (50×). Its
50
+ // description was a verbatim 38-char repeat on each tool; a single short shared
51
+ // constant trims that lossless repeat from the cached tools/list prefix without
52
+ // dropping the one fact that matters (it must be absolute, not relative).
53
+ const DIR_DESC = 'Absolute project path';
43
54
  export const TOOL_DEFINITIONS = [
44
55
  {
45
56
  name: 'orient',
@@ -53,7 +64,7 @@ export const TOOL_DEFINITIONS = [
53
64
  properties: {
54
65
  directory: {
55
66
  type: 'string',
56
- description: 'Absolute path to the project directory',
67
+ description: DIR_DESC,
57
68
  },
58
69
  task: {
59
70
  type: 'string',
@@ -63,6 +74,14 @@ export const TOOL_DEFINITIONS = [
63
74
  type: 'number',
64
75
  description: 'Number of relevant functions to return (default: 5)',
65
76
  },
77
+ tokenBudget: {
78
+ type: 'number',
79
+ description: 'Optional: cap relevantFunctions to ~this many tokens (highest-scored kept, exact duplicates collapsed); each item carries an `expand` handle for get_function_body',
80
+ },
81
+ lean: {
82
+ type: 'boolean',
83
+ description: 'Return only the navigation core (relevantFunctions + callPaths + specDomains); drop provenance/change-coupling/insertion-points/specs/decisions enrichment (each reachable via expand handles or dedicated tools). Lower per-call cost for shallow "who/where" lookups.',
84
+ },
66
85
  },
67
86
  required: ['directory', 'task'],
68
87
  },
@@ -91,8 +110,8 @@ export const TOOL_DEFINITIONS = [
91
110
  {
92
111
  name: 'get_architecture_overview',
93
112
  description: 'USE THIS WHEN: onboarding to an unknown codebase, or before planning a large feature. ' +
94
- 'Returns domain clusters, cross-cluster dependencies, global entry points, and critical hubs ' +
95
- 'faster than reading package.json + directory tree yourself. Run analyze_codebase first.',
113
+ 'Returns domain clusters, cross-cluster dependencies, global entry points, and critical hubs. ' +
114
+ 'Run analyze_codebase first.',
96
115
  inputSchema: {
97
116
  type: 'object',
98
117
  properties: {
@@ -125,14 +144,13 @@ export const TOOL_DEFINITIONS = [
125
144
  name: 'get_call_graph',
126
145
  description: 'Return the call graph for a project: hub functions (high fan-in), ' +
127
146
  'entry points (no internal callers), and architectural layer violations. ' +
128
- 'Supports TypeScript, JavaScript, Python, Go, Rust, Ruby, Java, C++. ' +
129
147
  'Run analyze_codebase first.',
130
148
  inputSchema: {
131
149
  type: 'object',
132
150
  properties: {
133
151
  directory: {
134
152
  type: 'string',
135
- description: 'Absolute path to the project directory',
153
+ description: DIR_DESC,
136
154
  },
137
155
  },
138
156
  required: ['directory'],
@@ -144,7 +162,7 @@ export const TOOL_DEFINITIONS = [
144
162
  'Detects Type 1 (exact clones — identical after whitespace/comment normalization), ' +
145
163
  'Type 2 (structural clones — same structure with renamed variables), and ' +
146
164
  'Type 3 (near-clones with Jaccard similarity ≥ 0.7 on token n-grams). ' +
147
- 'No LLM calls required. Run analyze_codebase first.',
165
+ 'Run analyze_codebase first.',
148
166
  inputSchema: {
149
167
  type: 'object',
150
168
  properties: {
@@ -166,7 +184,7 @@ export const TOOL_DEFINITIONS = [
166
184
  properties: {
167
185
  directory: {
168
186
  type: 'string',
169
- description: 'Absolute path to the project directory',
187
+ description: DIR_DESC,
170
188
  },
171
189
  filePattern: {
172
190
  type: 'string',
@@ -189,7 +207,7 @@ export const TOOL_DEFINITIONS = [
189
207
  properties: {
190
208
  directory: {
191
209
  type: 'string',
192
- description: 'Absolute path to the project directory',
210
+ description: DIR_DESC,
193
211
  },
194
212
  functionName: {
195
213
  type: 'string',
@@ -227,7 +245,7 @@ export const TOOL_DEFINITIONS = [
227
245
  properties: {
228
246
  directory: {
229
247
  type: 'string',
230
- description: 'Absolute path to the project directory',
248
+ description: DIR_DESC,
231
249
  },
232
250
  entryFunction: {
233
251
  type: 'string',
@@ -260,7 +278,7 @@ export const TOOL_DEFINITIONS = [
260
278
  properties: {
261
279
  directory: {
262
280
  type: 'string',
263
- description: 'Absolute path to the project directory',
281
+ description: DIR_DESC,
264
282
  },
265
283
  domain: {
266
284
  type: 'string',
@@ -325,7 +343,7 @@ export const TOOL_DEFINITIONS = [
325
343
  properties: {
326
344
  directory: {
327
345
  type: 'string',
328
- description: 'Absolute path to the project directory',
346
+ description: DIR_DESC,
329
347
  },
330
348
  symbol: {
331
349
  type: 'string',
@@ -339,6 +357,107 @@ export const TOOL_DEFINITIONS = [
339
357
  required: ['directory', 'symbol'],
340
358
  },
341
359
  },
360
+ {
361
+ name: 'select_tests',
362
+ description: 'USE THIS WHEN: you changed code and want to know which tests to run — ' +
363
+ '"which tests cover parseConfig?", "what should I run for this diff?". ' +
364
+ 'Walks the call graph BACKWARD from the change to every test that transitively reaches it, ' +
365
+ 'with the reaching path per test. Deterministic, offline, no test run. ' +
366
+ 'It is an over-approximate PRIORITIZER (run these first), not a sound replacement for the full ' +
367
+ 'suite — the response states its confidence and coverage. Run analyze_codebase first.',
368
+ inputSchema: {
369
+ type: 'object',
370
+ properties: {
371
+ directory: { type: 'string', description: DIR_DESC },
372
+ changedSymbols: {
373
+ type: 'array',
374
+ items: { type: 'string' },
375
+ description: 'Changed function/method names. Provide this OR diffRef.',
376
+ },
377
+ diffRef: {
378
+ type: 'string',
379
+ description: 'Git ref to diff the working tree against (e.g. "HEAD", "main"). Provide this OR changedSymbols.',
380
+ },
381
+ maxDepth: { type: 'number', description: 'Backward reachability depth (default 12)' },
382
+ },
383
+ required: ['directory'],
384
+ },
385
+ },
386
+ {
387
+ name: 'find_dead_code',
388
+ description: 'USE THIS WHEN: "what code is unreachable / dead?", "is anything calling X?", or ' +
389
+ '"what becomes dead if I delete X?". Cross-language mark-and-sweep reachability from roots ' +
390
+ '(tests, imported symbols, route handlers, main) over the call graph. ' +
391
+ 'Pass ifDeleted to get the downstream-only-reachable set for a symbol. ' +
392
+ 'Results are confidence-tagged CANDIDATES, never deletion authority — dynamic dispatch, DI, ' +
393
+ 'and external consumers cause false positives, stated in the response. Run analyze_codebase first.',
394
+ inputSchema: {
395
+ type: 'object',
396
+ properties: {
397
+ directory: { type: 'string', description: DIR_DESC },
398
+ ifDeleted: { type: 'string', description: 'Symbol name — returns what becomes dead if it is deleted (delete-impact mode)' },
399
+ maxResults: { type: 'number', description: 'Max candidate-dead results (default 100)' },
400
+ filePattern: { type: 'string', description: 'Only report candidates whose file path contains this substring' },
401
+ },
402
+ required: ['directory'],
403
+ },
404
+ },
405
+ {
406
+ name: 'structural_diff',
407
+ description: 'USE THIS WHEN reviewing or refactoring a change: "what changed structurally?", ' +
408
+ '"whose callers are now stale?". A graph diff (complement to git diff) between two states ' +
409
+ '(working tree vs a ref, or two refs): functions/edges added & removed, signature changes, ' +
410
+ 'and the existing callers now STALE because a callee signature moved under them. ' +
411
+ 'Rename/move ambiguity is flagged, not guessed. Deterministic, offline. Run analyze_codebase ' +
412
+ 'first for stale-caller analysis.',
413
+ inputSchema: {
414
+ type: 'object',
415
+ properties: {
416
+ directory: { type: 'string', description: DIR_DESC },
417
+ baseRef: { type: 'string', description: 'Old state to diff against (default "HEAD")' },
418
+ headRef: { type: 'string', description: 'New state (a git ref). Omit to use the working tree.' },
419
+ maxResults: { type: 'number', description: 'Cap reported items per category (default 200)' },
420
+ },
421
+ required: ['directory'],
422
+ },
423
+ },
424
+ {
425
+ name: 'get_change_coupling',
426
+ description: 'USE THIS WHEN: "what changes together with this file?" or "what is the most volatile code?". ' +
427
+ 'Mined from local git history (not the call graph): co-change coupling surfaces invisible ' +
428
+ 'coupling with no import/call edge (the config + parser that move in lockstep), and ' +
429
+ 'volatility/churn flags risky high-change code. Pass a file for its coupling, or omit for the ' +
430
+ 'most-volatile overview. Advisory signal (correlation, not causation); bulk commits filtered. ' +
431
+ 'Run analyze_codebase first.',
432
+ inputSchema: {
433
+ type: 'object',
434
+ properties: {
435
+ directory: { type: 'string', description: DIR_DESC },
436
+ file: { type: 'string', description: 'A file to query its coupling/volatility. Omit for the most-volatile overview.' },
437
+ limit: { type: 'number', description: 'Cap results (default 20)' },
438
+ },
439
+ required: ['directory'],
440
+ },
441
+ },
442
+ {
443
+ name: 'check_architecture',
444
+ description: 'USE THIS BEFORE adding an import to check it against the repo\'s architecture rules, or to ' +
445
+ 'list current architecture violations. Opt-in and inert unless the repo declares rules in ' +
446
+ '.openlore/architecture.json (layers / forbidden / allowedOnly) or via an "Invariant:" marker ' +
447
+ 'in a synced ADR. Pre-edit mode: pass {from, to} ("may a file under <from> import <to>?") for a ' +
448
+ 'deterministic allowed/denied + the governing rule + why, BEFORE you write the code. Scan mode: ' +
449
+ 'pass only {directory} for the full current-violations report. Cross-language, offline, ' +
450
+ 'deterministic; complements (does not replace) CI linters. Run analyze_codebase first.',
451
+ inputSchema: {
452
+ type: 'object',
453
+ properties: {
454
+ directory: { type: 'string', description: DIR_DESC },
455
+ from: { type: 'string', description: 'Pre-edit mode: the file that would gain the import (relative or absolute). Requires "to".' },
456
+ to: { type: 'string', description: 'Pre-edit mode: the target file path or exported symbol being imported. Requires "from".' },
457
+ },
458
+ required: ['directory'],
459
+ },
460
+ },
342
461
  {
343
462
  name: 'get_low_risk_refactor_candidates',
344
463
  description: 'Return the safest functions to refactor first: low fan-in (few callers), ' +
@@ -350,7 +469,7 @@ export const TOOL_DEFINITIONS = [
350
469
  properties: {
351
470
  directory: {
352
471
  type: 'string',
353
- description: 'Absolute path to the project directory',
472
+ description: DIR_DESC,
354
473
  },
355
474
  limit: {
356
475
  type: 'number',
@@ -375,7 +494,7 @@ export const TOOL_DEFINITIONS = [
375
494
  properties: {
376
495
  directory: {
377
496
  type: 'string',
378
- description: 'Absolute path to the project directory',
497
+ description: DIR_DESC,
379
498
  },
380
499
  limit: {
381
500
  type: 'number',
@@ -405,7 +524,7 @@ export const TOOL_DEFINITIONS = [
405
524
  properties: {
406
525
  directory: {
407
526
  type: 'string',
408
- description: 'Absolute path to the project directory',
527
+ description: DIR_DESC,
409
528
  },
410
529
  limit: {
411
530
  type: 'number',
@@ -431,7 +550,7 @@ export const TOOL_DEFINITIONS = [
431
550
  properties: {
432
551
  directory: {
433
552
  type: 'string',
434
- description: 'Absolute path to the project directory',
553
+ description: DIR_DESC,
435
554
  },
436
555
  filePath: {
437
556
  type: 'string',
@@ -452,7 +571,7 @@ export const TOOL_DEFINITIONS = [
452
571
  properties: {
453
572
  directory: {
454
573
  type: 'string',
455
- description: 'Absolute path to the project directory',
574
+ description: DIR_DESC,
456
575
  },
457
576
  filePath: {
458
577
  type: 'string',
@@ -478,7 +597,7 @@ export const TOOL_DEFINITIONS = [
478
597
  properties: {
479
598
  directory: {
480
599
  type: 'string',
481
- description: 'Absolute path to the project directory',
600
+ description: DIR_DESC,
482
601
  },
483
602
  description: {
484
603
  type: 'string',
@@ -509,7 +628,7 @@ export const TOOL_DEFINITIONS = [
509
628
  properties: {
510
629
  directory: {
511
630
  type: 'string',
512
- description: 'Absolute path to the project directory',
631
+ description: DIR_DESC,
513
632
  },
514
633
  query: {
515
634
  type: 'string',
@@ -527,6 +646,10 @@ export const TOOL_DEFINITIONS = [
527
646
  type: 'number',
528
647
  description: 'Only return functions with at least this many callers (hub filter)',
529
648
  },
649
+ tokenBudget: {
650
+ type: 'number',
651
+ description: 'Optional: cap results to ~this many tokens (highest-scored kept, exact duplicates collapsed); each hit carries an `expand` handle for get_function_body',
652
+ },
530
653
  },
531
654
  required: ['directory', 'query'],
532
655
  },
@@ -538,7 +661,7 @@ export const TOOL_DEFINITIONS = [
538
661
  inputSchema: {
539
662
  type: 'object',
540
663
  properties: {
541
- directory: { type: 'string', description: 'Absolute path to the project directory' },
664
+ directory: { type: 'string', description: DIR_DESC },
542
665
  },
543
666
  required: ['directory'],
544
667
  },
@@ -554,7 +677,7 @@ export const TOOL_DEFINITIONS = [
554
677
  properties: {
555
678
  directory: {
556
679
  type: 'string',
557
- description: 'Absolute path to the project directory',
680
+ description: DIR_DESC,
558
681
  },
559
682
  query: {
560
683
  type: 'string',
@@ -589,7 +712,7 @@ export const TOOL_DEFINITIONS = [
589
712
  properties: {
590
713
  directory: {
591
714
  type: 'string',
592
- description: 'Absolute path to the project directory',
715
+ description: DIR_DESC,
593
716
  },
594
717
  query: {
595
718
  type: 'string',
@@ -624,7 +747,7 @@ export const TOOL_DEFINITIONS = [
624
747
  inputSchema: {
625
748
  type: 'object',
626
749
  properties: {
627
- directory: { type: 'string', description: 'Absolute path to the project directory' },
750
+ directory: { type: 'string', description: DIR_DESC },
628
751
  domain: {
629
752
  type: 'string',
630
753
  description: 'Domain name as returned by list_spec_domains (e.g. "auth", "analyzer")',
@@ -642,7 +765,7 @@ export const TOOL_DEFINITIONS = [
642
765
  inputSchema: {
643
766
  type: 'object',
644
767
  properties: {
645
- directory: { type: 'string', description: 'Absolute path to the project directory' },
768
+ directory: { type: 'string', description: DIR_DESC },
646
769
  filePath: {
647
770
  type: 'string',
648
771
  description: 'File path relative to the project directory, e.g. "src/auth/jwt.ts"',
@@ -664,7 +787,7 @@ export const TOOL_DEFINITIONS = [
664
787
  inputSchema: {
665
788
  type: 'object',
666
789
  properties: {
667
- directory: { type: 'string', description: 'Absolute path to the project directory' },
790
+ directory: { type: 'string', description: DIR_DESC },
668
791
  filePath: {
669
792
  type: 'string',
670
793
  description: 'File path relative to the project root, e.g. "src/core/analyzer/vector-index.ts"',
@@ -691,7 +814,7 @@ export const TOOL_DEFINITIONS = [
691
814
  properties: {
692
815
  directory: {
693
816
  type: 'string',
694
- description: 'Absolute path to the project directory',
817
+ description: DIR_DESC,
695
818
  },
696
819
  description: {
697
820
  type: 'string',
@@ -725,7 +848,7 @@ export const TOOL_DEFINITIONS = [
725
848
  properties: {
726
849
  directory: {
727
850
  type: 'string',
728
- description: 'Absolute path to the project directory',
851
+ description: DIR_DESC,
729
852
  },
730
853
  storyFilePath: {
731
854
  type: 'string',
@@ -750,7 +873,7 @@ export const TOOL_DEFINITIONS = [
750
873
  inputSchema: {
751
874
  type: 'object',
752
875
  properties: {
753
- directory: { type: 'string', description: 'Absolute path to the project directory' },
876
+ directory: { type: 'string', description: DIR_DESC },
754
877
  query: {
755
878
  type: 'string',
756
879
  description: 'Optional text filter — returns only ADRs whose title or content contains this string',
@@ -771,7 +894,7 @@ export const TOOL_DEFINITIONS = [
771
894
  inputSchema: {
772
895
  type: 'object',
773
896
  properties: {
774
- directory: { type: 'string', description: 'Absolute path to the project directory' },
897
+ directory: { type: 'string', description: DIR_DESC },
775
898
  },
776
899
  required: ['directory'],
777
900
  },
@@ -788,7 +911,7 @@ export const TOOL_DEFINITIONS = [
788
911
  inputSchema: {
789
912
  type: 'object',
790
913
  properties: {
791
- directory: { type: 'string', description: 'Absolute path to the project directory' },
914
+ directory: { type: 'string', description: DIR_DESC },
792
915
  },
793
916
  required: ['directory'],
794
917
  },
@@ -804,7 +927,7 @@ export const TOOL_DEFINITIONS = [
804
927
  inputSchema: {
805
928
  type: 'object',
806
929
  properties: {
807
- directory: { type: 'string', description: 'Absolute path to the project directory' },
930
+ directory: { type: 'string', description: DIR_DESC },
808
931
  },
809
932
  required: ['directory'],
810
933
  },
@@ -820,7 +943,7 @@ export const TOOL_DEFINITIONS = [
820
943
  inputSchema: {
821
944
  type: 'object',
822
945
  properties: {
823
- directory: { type: 'string', description: 'Absolute path to the project directory' },
946
+ directory: { type: 'string', description: DIR_DESC },
824
947
  },
825
948
  required: ['directory'],
826
949
  },
@@ -837,7 +960,7 @@ export const TOOL_DEFINITIONS = [
837
960
  inputSchema: {
838
961
  type: 'object',
839
962
  properties: {
840
- directory: { type: 'string', description: 'Absolute path to the project directory' },
963
+ directory: { type: 'string', description: DIR_DESC },
841
964
  },
842
965
  required: ['directory'],
843
966
  },
@@ -852,7 +975,7 @@ export const TOOL_DEFINITIONS = [
852
975
  inputSchema: {
853
976
  type: 'object',
854
977
  properties: {
855
- directory: { type: 'string', description: 'Absolute path to the project directory' },
978
+ directory: { type: 'string', description: DIR_DESC },
856
979
  },
857
980
  required: ['directory'],
858
981
  },
@@ -869,7 +992,7 @@ export const TOOL_DEFINITIONS = [
869
992
  inputSchema: {
870
993
  type: 'object',
871
994
  properties: {
872
- directory: { type: 'string', description: 'Absolute path to the project directory' },
995
+ directory: { type: 'string', description: DIR_DESC },
873
996
  maxUncovered: {
874
997
  type: 'number',
875
998
  description: 'Maximum uncovered functions to return (default: 50)',
@@ -897,7 +1020,7 @@ export const TOOL_DEFINITIONS = [
897
1020
  properties: {
898
1021
  directory: {
899
1022
  type: 'string',
900
- description: 'Absolute path to the project directory',
1023
+ description: DIR_DESC,
901
1024
  },
902
1025
  domains: {
903
1026
  type: 'array',
@@ -934,7 +1057,7 @@ export const TOOL_DEFINITIONS = [
934
1057
  properties: {
935
1058
  directory: {
936
1059
  type: 'string',
937
- description: 'Absolute path to the project directory',
1060
+ description: DIR_DESC,
938
1061
  },
939
1062
  domains: {
940
1063
  type: 'array',
@@ -959,7 +1082,7 @@ export const TOOL_DEFINITIONS = [
959
1082
  inputSchema: {
960
1083
  type: 'object',
961
1084
  properties: {
962
- directory: { type: 'string', description: 'Absolute path to the project directory' },
1085
+ directory: { type: 'string', description: DIR_DESC },
963
1086
  functionName: { type: 'string', description: 'Exact function or method name' },
964
1087
  filePath: {
965
1088
  type: 'string',
@@ -979,7 +1102,7 @@ export const TOOL_DEFINITIONS = [
979
1102
  inputSchema: {
980
1103
  type: 'object',
981
1104
  properties: {
982
- directory: { type: 'string', description: 'Absolute path to the project directory' },
1105
+ directory: { type: 'string', description: DIR_DESC },
983
1106
  functionName: { type: 'string', description: 'Function name to look up the community for' },
984
1107
  },
985
1108
  required: ['directory', 'functionName'],
@@ -996,7 +1119,7 @@ export const TOOL_DEFINITIONS = [
996
1119
  inputSchema: {
997
1120
  type: 'object',
998
1121
  properties: {
999
- directory: { type: 'string', description: 'Absolute path to the project directory' },
1122
+ directory: { type: 'string', description: DIR_DESC },
1000
1123
  base: {
1001
1124
  type: 'string',
1002
1125
  description: 'Git ref to diff against (default: HEAD). Use "HEAD~1" for last commit, "main" for branch diff.',
@@ -1015,7 +1138,7 @@ export const TOOL_DEFINITIONS = [
1015
1138
  inputSchema: {
1016
1139
  type: 'object',
1017
1140
  properties: {
1018
- directory: { type: 'string', description: 'Absolute path to the project directory' },
1141
+ directory: { type: 'string', description: DIR_DESC },
1019
1142
  title: { type: 'string', description: 'Short imperative statement, e.g. "Use UUIDs for decision IDs"' },
1020
1143
  rationale: { type: 'string', description: 'Why this decision was made' },
1021
1144
  consequences: { type: 'string', description: 'What changes as a result (optional)' },
@@ -1046,7 +1169,7 @@ export const TOOL_DEFINITIONS = [
1046
1169
  inputSchema: {
1047
1170
  type: 'object',
1048
1171
  properties: {
1049
- directory: { type: 'string', description: 'Absolute path to the project directory' },
1172
+ directory: { type: 'string', description: DIR_DESC },
1050
1173
  status: {
1051
1174
  type: 'string',
1052
1175
  enum: ['draft', 'consolidated', 'verified', 'phantom', 'approved', 'rejected', 'synced'],
@@ -1066,7 +1189,7 @@ export const TOOL_DEFINITIONS = [
1066
1189
  inputSchema: {
1067
1190
  type: 'object',
1068
1191
  properties: {
1069
- directory: { type: 'string', description: 'Absolute path to the project directory' },
1192
+ directory: { type: 'string', description: DIR_DESC },
1070
1193
  id: { type: 'string', description: '8-character decision ID from list_decisions' },
1071
1194
  note: { type: 'string', description: 'Optional review note' },
1072
1195
  },
@@ -1079,7 +1202,7 @@ export const TOOL_DEFINITIONS = [
1079
1202
  inputSchema: {
1080
1203
  type: 'object',
1081
1204
  properties: {
1082
- directory: { type: 'string', description: 'Absolute path to the project directory' },
1205
+ directory: { type: 'string', description: DIR_DESC },
1083
1206
  id: { type: 'string', description: '8-character decision ID from list_decisions' },
1084
1207
  note: { type: 'string', description: 'Optional reason for rejection' },
1085
1208
  },
@@ -1096,7 +1219,7 @@ export const TOOL_DEFINITIONS = [
1096
1219
  inputSchema: {
1097
1220
  type: 'object',
1098
1221
  properties: {
1099
- directory: { type: 'string', description: 'Absolute path to the project directory' },
1222
+ directory: { type: 'string', description: DIR_DESC },
1100
1223
  dryRun: { type: 'boolean', description: 'Preview without writing files (default: false)' },
1101
1224
  id: { type: 'string', description: 'Sync only this specific decision ID (default: all approved)' },
1102
1225
  },
@@ -1116,7 +1239,7 @@ const TOOL_ANNOTATIONS = {
1116
1239
  orient: _RO, analyze_codebase: _RWI, get_architecture_overview: _RO,
1117
1240
  get_refactor_report: _RO, get_call_graph: _RO, get_duplicate_report: _RO,
1118
1241
  get_signatures: _RO, get_subgraph: _RO, trace_execution_path: _RO,
1119
- get_mapping: _RO, check_spec_drift: _RO, analyze_impact: _RO,
1242
+ 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
1243
  get_low_risk_refactor_candidates: _RO, get_leaf_functions: _RO,
1121
1244
  get_critical_hubs: _RO, get_function_skeleton: _RO, get_god_functions: _RO,
1122
1245
  suggest_insertion_points: _RO, search_code: _RO, list_spec_domains: _RO,
@@ -1129,6 +1252,21 @@ const TOOL_ANNOTATIONS = {
1129
1252
  detect_changes: _RO, record_decision: _RW, list_decisions: _RO,
1130
1253
  approve_decision: _RWI, reject_decision: _RWI, sync_decisions: _RWI,
1131
1254
  };
1255
+ // Tools that touch external entities (LLM / network) → openWorldHint: true.
1256
+ // Everything else is local, deterministic, closed-world analysis.
1257
+ const OPEN_WORLD_TOOLS = new Set(['generate_tests', 'generate_change_proposal', 'annotate_story']);
1258
+ /** Human-readable title from a snake_case tool name (spec-11 annotations). */
1259
+ function toolTitle(name) {
1260
+ return name.split('_').map(w => (w ? w[0].toUpperCase() + w.slice(1) : w)).join(' ');
1261
+ }
1262
+ /** Full MCP `annotations` for a tool: read/write hints + title + openWorldHint (spec-11). */
1263
+ export function toolAnnotations(name) {
1264
+ return {
1265
+ title: toolTitle(name),
1266
+ ...(TOOL_ANNOTATIONS[name] ?? _RO),
1267
+ openWorldHint: OPEN_WORLD_TOOLS.has(name),
1268
+ };
1269
+ }
1132
1270
  const MINIMAL_TOOLS = new Set([
1133
1271
  'orient', 'search_code', 'record_decision', 'detect_changes', 'check_spec_drift',
1134
1272
  ]);
@@ -1184,7 +1322,7 @@ async function startMcpServer(options = {}) {
1184
1322
  const { version: pkgVersion } = _require('../../../package.json');
1185
1323
  const server = new Server({ name: 'openlore', version: pkgVersion }, { capabilities: { tools: {} } });
1186
1324
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
1187
- tools: activeTools.map(t => ({ ...t, annotations: TOOL_ANNOTATIONS[t.name] })),
1325
+ tools: activeTools.map(t => ({ ...t, annotations: toolAnnotations(t.name) })),
1188
1326
  }));
1189
1327
  // Per-session epistemic lease tracker — re-initialized when directory changes.
1190
1328
  let tracker;
@@ -1197,8 +1335,16 @@ async function startMcpServer(options = {}) {
1197
1335
  server.setRequestHandler(InitializeRequestSchema, async (request) => {
1198
1336
  agentName = request.params.clientInfo?.name ?? 'unknown';
1199
1337
  agentVersion = request.params.clientInfo?.version ?? 'unknown';
1338
+ // Protocol negotiation (spec-12): echo the client's requested version when we
1339
+ // support it (per the SDK's pinned set), else offer our latest supported one.
1340
+ const requested = request.params.protocolVersion;
1341
+ const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(requested)
1342
+ ? requested
1343
+ : LATEST_PROTOCOL_VERSION;
1200
1344
  return {
1201
- protocolVersion: request.params.protocolVersion,
1345
+ protocolVersion,
1346
+ // Honest capabilities: only `tools`. No `listChanged` — the tool list is
1347
+ // static per session, so we don't advertise a capability we don't implement.
1202
1348
  capabilities: { tools: {} },
1203
1349
  serverInfo: { name: 'openlore', version: _pkgVersion },
1204
1350
  };
@@ -1225,6 +1371,17 @@ async function startMcpServer(options = {}) {
1225
1371
  const _dir = args.directory;
1226
1372
  const directory = typeof _dir === 'string' ? _dir : '';
1227
1373
  const _t0 = Date.now();
1374
+ // Input validation (spec-10) against the tool's own declared inputSchema, before
1375
+ // dispatch. Invalid args become a JSON-RPC -32602 error (spec-12), not an
1376
+ // isError tool result — a malformed request is a protocol error, not a tool failure.
1377
+ {
1378
+ const toolDef = TOOL_DEFINITIONS.find(t => t.name === name);
1379
+ const argError = toolDef ? validateToolArgs(args, toolDef.inputSchema) : null;
1380
+ if (argError) {
1381
+ emit(directory, 'mcp', { event: 'tool_error', tool: name, ms: Date.now() - _t0, agent: agentName, code: 'INVALID_ARGS', error: argError });
1382
+ throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for "${name}": ${argError}`);
1383
+ }
1384
+ }
1228
1385
  try {
1229
1386
  const filePath = args.filePath;
1230
1387
  // Init (or re-init when project directory changes between calls)
@@ -1236,205 +1393,240 @@ async function startMcpServer(options = {}) {
1236
1393
  if (tracker && directory)
1237
1394
  updateTracker(tracker, name, directory, typeof filePath === 'string' ? filePath : undefined);
1238
1395
  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
- });
1396
+ let _unknownTool = false;
1397
+ // Per-tool timeout (spec-10): race the dispatch against the tool's budget so a
1398
+ // pathological hang can never wedge the server. Slow tools (analysis, LLM) have
1399
+ // generous overrides in MCP_TOOL_TIMEOUT_OVERRIDES.
1400
+ await withToolTimeout((async () => {
1401
+ if (name === 'orient') {
1402
+ const { task, limit = 5, tokenBudget, lean } = args;
1403
+ result = await handleOrient(directory, task, limit, tokenBudget, lean);
1404
+ if (result && typeof result === 'object') {
1405
+ const r = result;
1406
+ emit(directory, 'orient', {
1407
+ event: 'orient_call',
1408
+ agent: agentName,
1409
+ functions: Array.isArray(r['relevantFunctions']) ? r['relevantFunctions'].length : 0,
1410
+ files: Array.isArray(r['relevantFiles']) ? r['relevantFiles'].length : 0,
1411
+ spec_domains: Array.isArray(r['specDomains']) ? r['specDomains'].length : 0,
1412
+ insertion_points: Array.isArray(r['insertionPoints']) ? r['insertionPoints'].length : 0,
1413
+ });
1414
+ }
1252
1415
  }
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 {
1416
+ else if (name === 'analyze_codebase') {
1417
+ const { directory, force = false } = args;
1418
+ result = await handleAnalyzeCodebase(directory, force);
1419
+ }
1420
+ else if (name === 'get_architecture_overview') {
1421
+ const { directory } = args;
1422
+ result = await handleGetArchitectureOverview(directory);
1423
+ }
1424
+ else if (name === 'get_refactor_report') {
1425
+ const { directory } = args;
1426
+ result = await handleGetRefactorReport(directory);
1427
+ }
1428
+ else if (name === 'get_call_graph') {
1429
+ const { directory } = args;
1430
+ result = await handleGetCallGraph(directory);
1431
+ }
1432
+ else if (name === 'get_signatures') {
1433
+ const { directory, filePattern } = args;
1434
+ result = await handleGetSignatures(directory, filePattern);
1435
+ }
1436
+ else if (name === 'get_subgraph') {
1437
+ const { directory, functionName, direction = 'downstream', maxDepth = 3, format = 'json' } = args;
1438
+ result = await handleGetSubgraph(directory, functionName, direction, maxDepth, format);
1439
+ }
1440
+ else if (name === 'trace_execution_path') {
1441
+ const { directory, entryFunction, targetFunction, maxDepth = 6, maxPaths = 10 } = args;
1442
+ result = await handleTraceExecutionPath(directory, entryFunction, targetFunction, maxDepth, maxPaths);
1443
+ }
1444
+ else if (name === 'get_mapping') {
1445
+ const { directory, domain, orphansOnly } = args;
1446
+ result = await handleGetMapping(directory, domain, orphansOnly);
1447
+ }
1448
+ else if (name === 'analyze_impact') {
1449
+ const { directory, symbol, depth = 2 } = args;
1450
+ result = await handleAnalyzeImpact(directory, symbol, depth);
1451
+ }
1452
+ else if (name === 'select_tests') {
1453
+ const { directory, changedSymbols, diffRef, maxDepth } = args;
1454
+ result = await handleSelectTests({ directory, changedSymbols, diffRef, maxDepth });
1455
+ }
1456
+ else if (name === 'find_dead_code') {
1457
+ const { directory, ifDeleted, maxResults, filePattern } = args;
1458
+ result = await handleFindDeadCode({ directory, ifDeleted, maxResults, filePattern });
1459
+ }
1460
+ else if (name === 'structural_diff') {
1461
+ const { directory, baseRef, headRef, maxResults } = args;
1462
+ result = await handleStructuralDiff({ directory, baseRef, headRef, maxResults });
1463
+ }
1464
+ else if (name === 'get_change_coupling') {
1465
+ const { directory, file, limit } = args;
1466
+ result = await handleGetChangeCoupling({ directory, file, limit });
1467
+ }
1468
+ else if (name === 'check_architecture') {
1469
+ const { directory, from, to } = args;
1470
+ result = await handleCheckArchitecture({ directory, from, to });
1471
+ }
1472
+ else if (name === 'get_low_risk_refactor_candidates') {
1473
+ const { directory, limit = 5, filePattern } = args;
1474
+ result = await handleGetLowRiskRefactorCandidates(directory, limit, filePattern);
1475
+ }
1476
+ else if (name === 'get_leaf_functions') {
1477
+ const { directory, limit = 20, filePattern, sortBy = 'fanIn' } = args;
1478
+ result = await handleGetLeafFunctions(directory, limit, filePattern, sortBy);
1479
+ }
1480
+ else if (name === 'get_critical_hubs') {
1481
+ const { directory, limit = 10, minFanIn = 3 } = args;
1482
+ result = await handleGetCriticalHubs(directory, limit, minFanIn);
1483
+ }
1484
+ else if (name === 'get_duplicate_report') {
1485
+ const { directory } = args;
1486
+ result = await handleGetDuplicateReport(directory);
1487
+ }
1488
+ else if (name === 'get_function_skeleton') {
1489
+ const { directory, filePath } = args;
1490
+ result = await handleGetFunctionSkeleton(directory, filePath);
1491
+ }
1492
+ else if (name === 'get_god_functions') {
1493
+ const { directory, filePath, fanOutThreshold = 8 } = args;
1494
+ result = await handleGetGodFunctions(directory, filePath, fanOutThreshold);
1495
+ }
1496
+ else if (name === 'check_spec_drift') {
1497
+ const { directory, base = 'auto', files = [], domains = [], failOn = 'warning', maxFiles = DEFAULT_DRIFT_MAX_FILES } = args;
1498
+ result = await handleCheckSpecDrift(directory, base, files, domains, failOn, maxFiles);
1499
+ }
1500
+ else if (name === 'search_code') {
1501
+ const { directory, query, limit = 10, language, minFanIn, tokenBudget } = args;
1502
+ result = await handleSearchCode(directory, query, limit, language, minFanIn, tokenBudget);
1503
+ }
1504
+ else if (name === 'suggest_insertion_points') {
1505
+ const { directory, description, limit = 5, language } = args;
1506
+ result = await handleSuggestInsertionPoints(directory, description, limit, language);
1507
+ }
1508
+ else if (name === 'search_specs') {
1509
+ const { directory, query, limit = 10, domain, section } = args;
1510
+ result = await handleSearchSpecs(directory, query, limit, domain, section);
1511
+ }
1512
+ else if (name === 'search_unified') {
1513
+ const { directory, query, limit = 10, language, domain, section } = args;
1514
+ result = await handleUnifiedSearch(directory, query, limit, language, domain, section);
1515
+ }
1516
+ else if (name === 'list_spec_domains') {
1517
+ const { directory } = args;
1518
+ result = await handleListSpecDomains(directory);
1519
+ }
1520
+ else if (name === 'get_spec') {
1521
+ const { directory, domain } = args;
1522
+ result = await handleGetSpec(directory, domain);
1523
+ }
1524
+ else if (name === 'get_function_body') {
1525
+ const { directory, filePath, functionName } = args;
1526
+ result = await handleGetFunctionBody(directory, filePath, functionName);
1527
+ }
1528
+ else if (name === 'get_file_dependencies') {
1529
+ const { directory, filePath, direction = 'both' } = args;
1530
+ result = await handleGetFileDependencies(directory, filePath, direction);
1531
+ }
1532
+ else if (name === 'generate_change_proposal') {
1533
+ const { directory, description, slug, storyContent } = args;
1534
+ result = await handleGenerateChangeProposal(directory, description, slug, storyContent);
1535
+ }
1536
+ else if (name === 'annotate_story') {
1537
+ const { directory, storyFilePath, description } = args;
1538
+ result = await handleAnnotateStory(directory, storyFilePath, description);
1539
+ }
1540
+ else if (name === 'get_decisions') {
1541
+ const { directory, query } = args;
1542
+ result = await handleGetDecisions(directory, query);
1543
+ }
1544
+ else if (name === 'get_route_inventory') {
1545
+ const { directory } = args;
1546
+ result = await handleGetRouteInventory(directory);
1547
+ }
1548
+ else if (name === 'get_middleware_inventory') {
1549
+ const { directory } = args;
1550
+ result = await handleGetMiddlewareInventory(directory);
1551
+ }
1552
+ else if (name === 'get_schema_inventory') {
1553
+ const { directory } = args;
1554
+ result = await handleGetSchemaInventory(directory);
1555
+ }
1556
+ else if (name === 'get_ui_components') {
1557
+ const { directory } = args;
1558
+ result = await handleGetUIComponents(directory);
1559
+ }
1560
+ else if (name === 'get_env_vars') {
1561
+ const { directory } = args;
1562
+ result = await handleGetEnvVars(directory);
1563
+ }
1564
+ else if (name === 'get_external_packages') {
1565
+ const { directory } = args;
1566
+ result = await handleGetExternalPackages(directory);
1567
+ }
1568
+ else if (name === 'audit_spec_coverage') {
1569
+ const { directory, maxUncovered = 50, hubThreshold = 5 } = args;
1570
+ result = await handleAuditSpecCoverage(directory, maxUncovered, hubThreshold);
1571
+ }
1572
+ else if (name === 'generate_tests') {
1573
+ const { directory, domains, framework, useLlm, dryRun } = args;
1574
+ result = await handleGenerateTests({ directory, domains, framework, useLlm, dryRun });
1575
+ }
1576
+ else if (name === 'get_test_coverage') {
1577
+ const { directory, domains, minCoverage } = args;
1578
+ result = await handleGetTestCoverage({ directory, domains, minCoverage });
1579
+ }
1580
+ else if (name === 'get_minimal_context') {
1581
+ const { directory, functionName, filePath } = args;
1582
+ result = await handleGetMinimalContext(directory, functionName, filePath);
1583
+ }
1584
+ else if (name === 'get_cluster') {
1585
+ const { directory, functionName } = args;
1586
+ result = await handleGetCluster(directory, functionName);
1587
+ }
1588
+ else if (name === 'detect_changes') {
1589
+ const { directory, base } = args;
1590
+ result = await handleDetectChanges(directory, base);
1591
+ }
1592
+ else if (name === 'record_decision') {
1593
+ const { directory, title, rationale, consequences, affectedFiles, supersedes, scope } = args;
1594
+ result = await handleRecordDecision(directory, title, rationale, consequences, affectedFiles, supersedes, scope);
1595
+ }
1596
+ else if (name === 'list_decisions') {
1597
+ const { directory, status } = args;
1598
+ result = await handleListDecisions(directory, status);
1599
+ }
1600
+ else if (name === 'approve_decision') {
1601
+ const { directory, id, note } = args;
1602
+ result = await handleApproveDecision(directory, id, note);
1603
+ }
1604
+ else if (name === 'reject_decision') {
1605
+ const { directory, id, note } = args;
1606
+ result = await handleRejectDecision(directory, id, note);
1607
+ }
1608
+ else if (name === 'sync_decisions') {
1609
+ const { directory, dryRun = false, id } = args;
1610
+ result = await handleSyncDecisions(directory, dryRun, id);
1611
+ }
1612
+ else {
1613
+ _unknownTool = true;
1614
+ }
1615
+ })(), name);
1616
+ if (_unknownTool) {
1431
1617
  return {
1432
1618
  content: [{ type: 'text', text: `Unknown tool: ${name}` }],
1433
1619
  isError: true,
1434
1620
  };
1435
1621
  }
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);
1622
+ const rawText = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
1623
+ // Output cap (spec-10): truncate deterministically with a how-to-narrow note
1624
+ // rather than silently dropping data or blowing the agent's context.
1625
+ const { text, truncated } = capOutput(rawText, MCP_TOOL_MAX_BYTES);
1626
+ emit(directory, 'mcp', {
1627
+ event: 'tool_call', tool: name, ms: Date.now() - _t0, agent: agentName, agent_version: agentVersion,
1628
+ bytes: Buffer.byteLength(text, 'utf8'), outcome: truncated ? 'truncated' : 'ok',
1629
+ });
1438
1630
  const signal = tracker ? getFreshnessSignal(tracker) : null;
1439
1631
  // Freshness signal is a separate content item — never concatenated into
1440
1632
  // the result body — so structured outputs (JSON, patches) are not corrupted.
@@ -1446,9 +1638,17 @@ async function startMcpServer(options = {}) {
1446
1638
  return { content };
1447
1639
  }
1448
1640
  catch (err) {
1449
- emit(directory, 'mcp', { event: 'tool_error', tool: name, ms: Date.now() - _t0, agent: agentName, error: sanitizeMcpError(err) });
1641
+ // A thrown McpError is a protocol-level error (e.g. -32602) let the SDK
1642
+ // serialize it as a JSON-RPC error response, not a tool isError result.
1643
+ if (err instanceof McpError)
1644
+ throw err;
1645
+ // Error normalization (spec-10): a stable code taxonomy, distinguishing
1646
+ // "repo not analyzed yet" (actionable) from real failures and timeouts.
1647
+ const code = classifyToolError(err);
1648
+ const message = sanitizeMcpError(err);
1649
+ emit(directory, 'mcp', { event: 'tool_error', tool: name, ms: Date.now() - _t0, agent: agentName, code, outcome: 'error', error: message });
1450
1650
  return {
1451
- content: [{ type: 'text', text: `Tool error: ${sanitizeMcpError(err)}` }],
1651
+ content: [{ type: 'text', text: `Tool error [${code}]: ${message}` }],
1452
1652
  isError: true,
1453
1653
  };
1454
1654
  }