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.
- package/README.md +122 -20
- package/dist/cli/commands/mcp.d.ts +681 -0
- package/dist/cli/commands/mcp.d.ts.map +1 -1
- package/dist/cli/commands/mcp.js +384 -200
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/constants.d.ts +6 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +14 -0
- package/dist/constants.js.map +1 -1
- package/dist/core/analyzer/artifact-generator.d.ts.map +1 -1
- package/dist/core/analyzer/artifact-generator.js +59 -4
- package/dist/core/analyzer/artifact-generator.js.map +1 -1
- package/dist/core/analyzer/call-graph.d.ts +19 -1
- package/dist/core/analyzer/call-graph.d.ts.map +1 -1
- package/dist/core/analyzer/call-graph.js +128 -28
- package/dist/core/analyzer/call-graph.js.map +1 -1
- package/dist/core/architecture/check.d.ts +63 -0
- package/dist/core/architecture/check.d.ts.map +1 -0
- package/dist/core/architecture/check.js +192 -0
- package/dist/core/architecture/check.js.map +1 -0
- package/dist/core/architecture/rules.d.ts +73 -0
- package/dist/core/architecture/rules.d.ts.map +1 -0
- package/dist/core/architecture/rules.js +201 -0
- package/dist/core/architecture/rules.js.map +1 -0
- package/dist/core/decisions/project.d.ts +59 -0
- package/dist/core/decisions/project.d.ts.map +1 -0
- package/dist/core/decisions/project.js +68 -0
- package/dist/core/decisions/project.js.map +1 -0
- package/dist/core/decisions/verifier.d.ts +10 -0
- package/dist/core/decisions/verifier.d.ts.map +1 -1
- package/dist/core/decisions/verifier.js +48 -5
- package/dist/core/decisions/verifier.js.map +1 -1
- package/dist/core/provenance/change-coupling.d.ts +68 -0
- package/dist/core/provenance/change-coupling.d.ts.map +1 -0
- package/dist/core/provenance/change-coupling.js +134 -0
- package/dist/core/provenance/change-coupling.js.map +1 -0
- package/dist/core/provenance/git-provenance.d.ts +67 -0
- package/dist/core/provenance/git-provenance.d.ts.map +1 -0
- package/dist/core/provenance/git-provenance.js +177 -0
- package/dist/core/provenance/git-provenance.js.map +1 -0
- package/dist/core/provenance/project.d.ts +37 -0
- package/dist/core/provenance/project.d.ts.map +1 -0
- package/dist/core/provenance/project.js +46 -0
- package/dist/core/provenance/project.js.map +1 -0
- package/dist/core/services/edge-store.d.ts +41 -0
- package/dist/core/services/edge-store.d.ts.map +1 -1
- package/dist/core/services/edge-store.js +251 -3
- package/dist/core/services/edge-store.js.map +1 -1
- package/dist/core/services/llm-service.d.ts +9 -0
- package/dist/core/services/llm-service.d.ts.map +1 -1
- package/dist/core/services/llm-service.js +15 -4
- package/dist/core/services/llm-service.js.map +1 -1
- package/dist/core/services/mcp-handlers/architecture.d.ts +19 -0
- package/dist/core/services/mcp-handlers/architecture.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/architecture.js +104 -0
- package/dist/core/services/mcp-handlers/architecture.js.map +1 -0
- package/dist/core/services/mcp-handlers/change-coupling.d.ts +16 -0
- package/dist/core/services/mcp-handlers/change-coupling.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/change-coupling.js +57 -0
- package/dist/core/services/mcp-handlers/change-coupling.js.map +1 -0
- package/dist/core/services/mcp-handlers/graph.d.ts +27 -0
- package/dist/core/services/mcp-handlers/graph.d.ts.map +1 -1
- package/dist/core/services/mcp-handlers/graph.js +98 -16
- package/dist/core/services/mcp-handlers/graph.js.map +1 -1
- package/dist/core/services/mcp-handlers/orient.d.ts.map +1 -1
- package/dist/core/services/mcp-handlers/orient.js +122 -2
- package/dist/core/services/mcp-handlers/orient.js.map +1 -1
- package/dist/core/services/mcp-handlers/reachability.d.ts +30 -0
- package/dist/core/services/mcp-handlers/reachability.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/reachability.js +222 -0
- package/dist/core/services/mcp-handlers/reachability.js.map +1 -0
- package/dist/core/services/mcp-handlers/structural-diff.d.ts +31 -0
- package/dist/core/services/mcp-handlers/structural-diff.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/structural-diff.js +268 -0
- package/dist/core/services/mcp-handlers/structural-diff.js.map +1 -0
- package/dist/core/services/mcp-handlers/test-impact.d.ts +34 -0
- package/dist/core/services/mcp-handlers/test-impact.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/test-impact.js +221 -0
- package/dist/core/services/mcp-handlers/test-impact.js.map +1 -0
- package/dist/core/services/mcp-handlers/tool-guard.d.ts +45 -0
- package/dist/core/services/mcp-handlers/tool-guard.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/tool-guard.js +81 -0
- package/dist/core/services/mcp-handlers/tool-guard.js.map +1 -0
- package/dist/core/services/mcp-handlers/utils.d.ts.map +1 -1
- package/dist/core/services/mcp-handlers/utils.js +15 -1
- package/dist/core/services/mcp-handlers/utils.js.map +1 -1
- package/dist/core/services/mcp-watcher.d.ts.map +1 -1
- package/dist/core/services/mcp-watcher.js +9 -0
- package/dist/core/services/mcp-watcher.js.map +1 -1
- package/package.json +8 -8
package/dist/cli/commands/mcp.js
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
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
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
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
|
-
|
|
1437
|
-
|
|
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
|
-
|
|
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: ${
|
|
1635
|
+
content: [{ type: 'text', text: `Tool error [${code}]: ${message}` }],
|
|
1452
1636
|
isError: true,
|
|
1453
1637
|
};
|
|
1454
1638
|
}
|