ctxman 3.1.1 → 3.2.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/.ctxman/llm-profiles.json +6 -6
- package/README.md +16 -10
- package/bin/cli.js +109 -10
- package/bin/cm-ask.js +64 -6
- package/bin/cm-audit.js +76 -0
- package/bin/cm-count.js +45 -6
- package/bin/cm-diagnose.js +199 -0
- package/bin/cm-focus.js +322 -0
- package/bin/cm-map.js +71 -18
- package/bin/cm-pack.js +36 -5
- package/bin/cm-policy.js +107 -2
- package/bin/cm-pr.js +18 -3
- package/bin/cm-search.js +84 -19
- package/bin/cm-task.js +192 -0
- package/bin/cm-who.js +119 -0
- package/bin/cm-why.js +41 -5
- package/index.d.ts +940 -1
- package/index.js +105 -1
- package/lib/analyzers/token-calculator.js +76 -4
- package/lib/api/mcp/server.js +843 -29
- package/lib/cache/CacheManager.js +161 -47
- package/lib/core/Analyzer.js +61 -5
- package/lib/count/index.js +74 -14
- package/lib/diagnose/index.js +712 -0
- package/lib/focus/index.js +790 -0
- package/lib/map/call-graph.js +570 -0
- package/lib/map/graph.js +30 -5
- package/lib/map/index.js +12 -4
- package/lib/map/rank.js +45 -7
- package/lib/map/render.js +89 -28
- package/lib/pack/index.js +20 -1
- package/lib/pack/select.js +8 -1
- package/lib/policy/audit.js +3 -1
- package/lib/policy/index.js +5 -0
- package/lib/policy/workspace.js +254 -0
- package/lib/pr/pr-context.js +292 -31
- package/lib/pr/pr-renderer.js +25 -10
- package/lib/rag/EmbeddingProvider.js +139 -2
- package/lib/rag/VectorStoreFactory.js +10 -4
- package/lib/rag/ask.js +186 -17
- package/lib/rag/stores/SqliteVectorStore.js +595 -0
- package/lib/search/hybrid.js +324 -0
- package/lib/search/symbols.js +75 -11
- package/lib/task/index.js +630 -0
- package/lib/utils/file-utils.js +1 -0
- package/lib/utils/multi-repo-manager.js +4 -1
- package/lib/watch/FileWatcher.js +121 -31
- package/lib/watch/IncrementalAnalyzer.js +964 -16
- package/lib/who/index.js +893 -0
- package/lib/why/analyzer-bridge.js +223 -0
- package/lib/why/discussions.js +314 -0
- package/lib/why/index.js +356 -30
- package/lib/why/render.js +154 -9
- package/lib/workspace/index.js +1274 -0
- package/package.json +1 -1
package/lib/api/mcp/server.js
CHANGED
|
@@ -19,10 +19,18 @@ import { PromptProvider } from './prompts.js';
|
|
|
19
19
|
import { buildPrContext } from '../../pr/pr-context.js';
|
|
20
20
|
import { resolveRootDir, resolveSandboxPath } from './sandbox.js';
|
|
21
21
|
import { searchProject, DEFAULT_LIMIT, DEFAULT_MAX_TOKENS } from '../../search/bm25.js';
|
|
22
|
+
import { hybridSearch } from '../../search/hybrid.js';
|
|
22
23
|
import { getSymbol } from '../../search/symbols.js';
|
|
23
24
|
import { countForModel } from '../../count/index.js';
|
|
24
25
|
import { explainHistory } from '../../why/index.js';
|
|
25
26
|
import { packContext, describePack, TOOL_DEFAULT_BUDGET } from '../../pack/index.js';
|
|
27
|
+
import { focusContext, describeFocus, DEFAULT_FOCUS_DEPTH } from '../../focus/index.js';
|
|
28
|
+
import {
|
|
29
|
+
resolveWorkspaceRepos,
|
|
30
|
+
mapWorkspace,
|
|
31
|
+
searchWorkspace,
|
|
32
|
+
packWorkspace,
|
|
33
|
+
} from '../../workspace/index.js';
|
|
26
34
|
import {
|
|
27
35
|
loadPolicy,
|
|
28
36
|
guardExport,
|
|
@@ -31,8 +39,14 @@ import {
|
|
|
31
39
|
readAuditLog,
|
|
32
40
|
filterAuditEvents,
|
|
33
41
|
parseSince,
|
|
42
|
+
checkWorkspacePolicies,
|
|
43
|
+
readWorkspaceAuditLogs,
|
|
34
44
|
} from '../../policy/index.js';
|
|
35
45
|
import { askCodebase, DEFAULT_ASK_LIMIT } from '../../rag/ask.js';
|
|
46
|
+
import { buildTaskContext, DEFAULT_TASK_BUDGET } from '../../task/index.js';
|
|
47
|
+
import { diagnoseFailure, DEFAULT_DIAGNOSE_BUDGET } from '../../diagnose/index.js';
|
|
48
|
+
import { findExperts, DEFAULT_WHO_LIMIT } from '../../who/index.js';
|
|
49
|
+
import { IncrementalAnalyzer } from '../../watch/IncrementalAnalyzer.js';
|
|
36
50
|
import fs from 'fs';
|
|
37
51
|
import path from 'path';
|
|
38
52
|
import { createRequire } from 'module';
|
|
@@ -45,6 +59,7 @@ const FILE_PATH_TOOLS = new Set(['list_methods']);
|
|
|
45
59
|
const DEFAULT_AUDIT_LIMIT = 50;
|
|
46
60
|
// Characters of each ask_codebase snippet (the index stores whole files)
|
|
47
61
|
const ASK_SNIPPET_CHARS = 2000;
|
|
62
|
+
const DEFAULT_GRAPH_LIMIT = 50;
|
|
48
63
|
|
|
49
64
|
class MCPServer {
|
|
50
65
|
/**
|
|
@@ -69,21 +84,47 @@ class MCPServer {
|
|
|
69
84
|
}
|
|
70
85
|
);
|
|
71
86
|
|
|
72
|
-
this.analyzer = new Analyzer();
|
|
87
|
+
this.analyzer = new Analyzer({ cache: true });
|
|
73
88
|
this.contextBuilder = new ContextBuilder();
|
|
89
|
+
this.incrementalAnalyzers = new Map();
|
|
74
90
|
this.resourceProvider = new ResourceProvider(this.root ?? undefined);
|
|
75
91
|
this.promptProvider = new PromptProvider(this.analyzer, this.contextBuilder);
|
|
76
92
|
this.setupHandlers();
|
|
77
93
|
this.setupErrorHandling();
|
|
78
94
|
}
|
|
79
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Get or create a cached IncrementalAnalyzer instance bound to the project's SQLite knowledge graph.
|
|
98
|
+
* @param {string} projectPath
|
|
99
|
+
* @returns {IncrementalAnalyzer}
|
|
100
|
+
*/
|
|
101
|
+
getIncrementalAnalyzer(projectPath, repos = []) {
|
|
102
|
+
const resolved = path.resolve(projectPath);
|
|
103
|
+
const extraRepos = Array.isArray(repos)
|
|
104
|
+
? repos.filter(Boolean).map((r) => path.resolve(r))
|
|
105
|
+
: [];
|
|
106
|
+
const cacheKey = [resolved, ...extraRepos].join('::');
|
|
107
|
+
let inc = this.incrementalAnalyzers.get(cacheKey);
|
|
108
|
+
if (!inc) {
|
|
109
|
+
inc = new IncrementalAnalyzer({
|
|
110
|
+
projectRoot: resolved,
|
|
111
|
+
repos: extraRepos,
|
|
112
|
+
sync: true,
|
|
113
|
+
hydrateGraphOnStart: false,
|
|
114
|
+
});
|
|
115
|
+
this.incrementalAnalyzers.set(cacheKey, inc);
|
|
116
|
+
}
|
|
117
|
+
return inc;
|
|
118
|
+
}
|
|
119
|
+
|
|
80
120
|
setupHandlers() {
|
|
81
121
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
82
122
|
return {
|
|
83
123
|
tools: [
|
|
84
124
|
{
|
|
85
125
|
name: 'analyze_codebase',
|
|
86
|
-
description:
|
|
126
|
+
description:
|
|
127
|
+
'Analyze the codebase (single-repo or multi-repo workspace) and return statistics and token counts',
|
|
87
128
|
inputSchema: {
|
|
88
129
|
type: 'object',
|
|
89
130
|
properties: {
|
|
@@ -91,13 +132,20 @@ class MCPServer {
|
|
|
91
132
|
type: 'string',
|
|
92
133
|
description: 'Absolute path to the project root',
|
|
93
134
|
},
|
|
135
|
+
repos: {
|
|
136
|
+
type: 'array',
|
|
137
|
+
items: { type: 'string' },
|
|
138
|
+
description:
|
|
139
|
+
'Additional repository roots to analyze together in a multi-repository workspace',
|
|
140
|
+
},
|
|
94
141
|
},
|
|
95
142
|
required: ['path'],
|
|
96
143
|
},
|
|
97
144
|
},
|
|
98
145
|
{
|
|
99
146
|
name: 'generate_context',
|
|
100
|
-
description:
|
|
147
|
+
description:
|
|
148
|
+
'Generate optimized context for LLMs from the codebase (single-repo or multi-repo workspace)',
|
|
101
149
|
inputSchema: {
|
|
102
150
|
type: 'object',
|
|
103
151
|
properties: {
|
|
@@ -114,13 +162,20 @@ class MCPServer {
|
|
|
114
162
|
type: 'number',
|
|
115
163
|
description: 'Maximum tokens for the context',
|
|
116
164
|
},
|
|
165
|
+
repos: {
|
|
166
|
+
type: 'array',
|
|
167
|
+
items: { type: 'string' },
|
|
168
|
+
description:
|
|
169
|
+
'Additional repository roots to include in multi-repository context generation',
|
|
170
|
+
},
|
|
117
171
|
},
|
|
118
172
|
required: ['path'],
|
|
119
173
|
},
|
|
120
174
|
},
|
|
121
175
|
{
|
|
122
176
|
name: 'git_diff',
|
|
123
|
-
description:
|
|
177
|
+
description:
|
|
178
|
+
'Analyze git changes (single-repo or multi-repo workspace) and return diff context',
|
|
124
179
|
inputSchema: {
|
|
125
180
|
type: 'object',
|
|
126
181
|
properties: {
|
|
@@ -129,6 +184,12 @@ class MCPServer {
|
|
|
129
184
|
type: 'string',
|
|
130
185
|
description: 'Branch to compare against (default: main)',
|
|
131
186
|
},
|
|
187
|
+
repos: {
|
|
188
|
+
type: 'array',
|
|
189
|
+
items: { type: 'string' },
|
|
190
|
+
description:
|
|
191
|
+
'Additional repository roots to include in multi-repository git diff',
|
|
192
|
+
},
|
|
132
193
|
},
|
|
133
194
|
required: ['path'],
|
|
134
195
|
},
|
|
@@ -136,13 +197,17 @@ class MCPServer {
|
|
|
136
197
|
{
|
|
137
198
|
name: 'search_code',
|
|
138
199
|
description:
|
|
139
|
-
'Search source lines by regular expression (default) or plain substring (
|
|
200
|
+
'Search source lines by regular expression (default) or plain substring (single-repo or multi-repo workspace). "ranked" returns BM25-ranked methods/chunks; "semantic" runs vector embedding search projected onto chunks (with BM25 fallback); "hybrid" fuses BM25 and semantic embeddings via Reciprocal Rank Fusion (RRF).',
|
|
140
201
|
inputSchema: {
|
|
141
202
|
type: 'object',
|
|
142
203
|
properties: {
|
|
143
204
|
path: { type: 'string', description: 'Project root' },
|
|
144
205
|
query: { type: 'string', description: 'Search query' },
|
|
145
|
-
type: {
|
|
206
|
+
type: {
|
|
207
|
+
type: 'string',
|
|
208
|
+
enum: ['regex', 'substring', 'semantic', 'ranked', 'hybrid'],
|
|
209
|
+
default: 'regex',
|
|
210
|
+
},
|
|
146
211
|
limit: {
|
|
147
212
|
type: 'number',
|
|
148
213
|
description: `ranked: maximum number of chunks (default ${DEFAULT_LIMIT})`,
|
|
@@ -155,13 +220,19 @@ class MCPServer {
|
|
|
155
220
|
type: 'boolean',
|
|
156
221
|
description: 'ranked: redact secrets in the returned source (default true)',
|
|
157
222
|
},
|
|
223
|
+
repos: {
|
|
224
|
+
type: 'array',
|
|
225
|
+
items: { type: 'string' },
|
|
226
|
+
description: 'Additional repository roots to search across in the workspace',
|
|
227
|
+
},
|
|
158
228
|
},
|
|
159
229
|
required: ['path', 'query'],
|
|
160
230
|
},
|
|
161
231
|
},
|
|
162
232
|
{
|
|
163
233
|
name: 'list_methods',
|
|
164
|
-
description:
|
|
234
|
+
description:
|
|
235
|
+
'List all methods/functions in a given file, project, or multi-repository workspace',
|
|
165
236
|
inputSchema: {
|
|
166
237
|
type: 'object',
|
|
167
238
|
properties: {
|
|
@@ -171,7 +242,14 @@ class MCPServer {
|
|
|
171
242
|
},
|
|
172
243
|
file: {
|
|
173
244
|
type: 'string',
|
|
174
|
-
description:
|
|
245
|
+
description:
|
|
246
|
+
'Optional: Relative path (or <alias>/<path>) to a specific file within the project',
|
|
247
|
+
},
|
|
248
|
+
repos: {
|
|
249
|
+
type: 'array',
|
|
250
|
+
items: { type: 'string' },
|
|
251
|
+
description:
|
|
252
|
+
'Additional repository roots to list methods across in a multi-repository workspace',
|
|
175
253
|
},
|
|
176
254
|
},
|
|
177
255
|
required: ['path'],
|
|
@@ -195,6 +273,12 @@ class MCPServer {
|
|
|
195
273
|
description:
|
|
196
274
|
'Project-relative files or folders to rank higher, with the files next to them in the import graph',
|
|
197
275
|
},
|
|
276
|
+
repos: {
|
|
277
|
+
type: 'array',
|
|
278
|
+
items: { type: 'string' },
|
|
279
|
+
description:
|
|
280
|
+
'Additional repository roots to include in a multi-repository workspace map under one shared token budget',
|
|
281
|
+
},
|
|
198
282
|
},
|
|
199
283
|
required: ['path'],
|
|
200
284
|
},
|
|
@@ -202,7 +286,7 @@ class MCPServer {
|
|
|
202
286
|
{
|
|
203
287
|
name: 'pr_context',
|
|
204
288
|
description:
|
|
205
|
-
'Review context for a branch: summary, commit log, changed files, full source of changed methods, unified diff and related tests (secrets redacted)',
|
|
289
|
+
'Review context for a branch (single-repo or multi-repo workspace): summary, commit log, changed files, full source of changed methods, unified diff and related tests (secrets redacted)',
|
|
206
290
|
inputSchema: {
|
|
207
291
|
type: 'object',
|
|
208
292
|
properties: {
|
|
@@ -215,6 +299,12 @@ class MCPServer {
|
|
|
215
299
|
head: { type: 'string', description: 'Head branch or commit (default: HEAD)' },
|
|
216
300
|
budget: { type: 'number', description: 'Maximum tokens of the result' },
|
|
217
301
|
style: { type: 'string', enum: ['markdown', 'xml', 'json'], default: 'markdown' },
|
|
302
|
+
repos: {
|
|
303
|
+
type: 'array',
|
|
304
|
+
items: { type: 'string' },
|
|
305
|
+
description:
|
|
306
|
+
'Additional repository roots to include in multi-repository PR review context under one shared token budget',
|
|
307
|
+
},
|
|
218
308
|
},
|
|
219
309
|
required: ['path'],
|
|
220
310
|
},
|
|
@@ -222,7 +312,7 @@ class MCPServer {
|
|
|
222
312
|
{
|
|
223
313
|
name: 'get_symbol',
|
|
224
314
|
description:
|
|
225
|
-
'Full source of a method, function or class by name, with file and line range. Exact matches first, then case-insensitive; similar names are suggested when nothing matches. "Class.method" narrows to methods of a class.',
|
|
315
|
+
'Full source of a method, function or class by name, with file and line range (single-repo or multi-repo workspace). Exact matches first, then case-insensitive; similar names are suggested when nothing matches. "Class.method" narrows to methods of a class.',
|
|
226
316
|
inputSchema: {
|
|
227
317
|
type: 'object',
|
|
228
318
|
properties: {
|
|
@@ -230,12 +320,19 @@ class MCPServer {
|
|
|
230
320
|
name: { type: 'string', description: 'Symbol name, or Class.method' },
|
|
231
321
|
file: {
|
|
232
322
|
type: 'string',
|
|
233
|
-
description:
|
|
323
|
+
description:
|
|
324
|
+
'Optional: only look in this project-relative (or <alias>/<path>) file',
|
|
234
325
|
},
|
|
235
326
|
redact: {
|
|
236
327
|
type: 'boolean',
|
|
237
328
|
description: 'Redact secrets in the source (default true)',
|
|
238
329
|
},
|
|
330
|
+
repos: {
|
|
331
|
+
type: 'array',
|
|
332
|
+
items: { type: 'string' },
|
|
333
|
+
description:
|
|
334
|
+
'Additional repository roots to search for symbols across under one shared token budget',
|
|
335
|
+
},
|
|
239
336
|
},
|
|
240
337
|
required: ['path', 'name'],
|
|
241
338
|
},
|
|
@@ -243,7 +340,7 @@ class MCPServer {
|
|
|
243
340
|
{
|
|
244
341
|
name: 'count_tokens',
|
|
245
342
|
description:
|
|
246
|
-
"Token count of a project (or of a text) for a model, with the model's context window and whether it fits. Exact for OpenAI models; approximate for models with their own tokenizer (Claude, Gemini, ...). Nothing leaves the machine; for Claude's exact count run `ctxman count --model <id> --api`",
|
|
343
|
+
"Token count of a project or multi-repository workspace (or of a text) for a model, with the model's context window and whether it fits. Exact for OpenAI models; approximate for models with their own tokenizer (Claude, Gemini, ...). Nothing leaves the machine; for Claude's exact count run `ctxman count --model <id> --api`",
|
|
247
344
|
inputSchema: {
|
|
248
345
|
type: 'object',
|
|
249
346
|
properties: {
|
|
@@ -260,13 +357,19 @@ class MCPServer {
|
|
|
260
357
|
type: 'number',
|
|
261
358
|
description: 'How many of the largest files to list (default 10)',
|
|
262
359
|
},
|
|
360
|
+
repos: {
|
|
361
|
+
type: 'array',
|
|
362
|
+
items: { type: 'string' },
|
|
363
|
+
description:
|
|
364
|
+
'Additional repository roots to count across in a multi-repository workspace (ignored when text is given)',
|
|
365
|
+
},
|
|
263
366
|
},
|
|
264
367
|
},
|
|
265
368
|
},
|
|
266
369
|
{
|
|
267
370
|
name: 'explain_history',
|
|
268
371
|
description:
|
|
269
|
-
'Explain why code looks the way it does, from the local git history (offline): commits that changed a file, symbol or line range with their messages, referenced issues/PRs/tickets, blame ownership, files that change together with it, related docs/ADRs/comments, TODO notes and tests. Returns Markdown.',
|
|
372
|
+
'Explain why code looks the way it does, from the local git history (offline, single-repo or multi-repo workspace): commits that changed a file, symbol or line range with their messages, referenced issues/PRs/tickets, institutional memory citations (.plansmith/analyzer.sqlite), blame ownership, files that change together with it, related docs/ADRs/comments, TODO notes and tests. Returns Markdown.',
|
|
270
373
|
inputSchema: {
|
|
271
374
|
type: 'object',
|
|
272
375
|
properties: {
|
|
@@ -277,7 +380,7 @@ class MCPServer {
|
|
|
277
380
|
target: {
|
|
278
381
|
type: 'string',
|
|
279
382
|
description:
|
|
280
|
-
'File relative to path, optionally with a symbol or line range: "src/cart.js", "src/cart.js:total", "src/cart.js:Cart.total", "src/cart.js#L10-L40"',
|
|
383
|
+
'File relative to path (or <alias>/<path> in multi-repo mode), optionally with a symbol or line range: "src/cart.js", "src/cart.js:total", "src/cart.js:Cart.total", "src/cart.js#L10-L40"',
|
|
281
384
|
},
|
|
282
385
|
limit: {
|
|
283
386
|
type: 'number',
|
|
@@ -296,6 +399,32 @@ class MCPServer {
|
|
|
296
399
|
type: 'boolean',
|
|
297
400
|
description: "Include each commit's diff of the target (default false)",
|
|
298
401
|
},
|
|
402
|
+
discussions: {
|
|
403
|
+
type: 'boolean',
|
|
404
|
+
description:
|
|
405
|
+
'Fetch referenced GitHub/GitLab issue, PR and MR discussions over HTTPS (default false; uses GITHUB_TOKEN/GH_TOKEN or GITLAB_TOKEN when set)',
|
|
406
|
+
},
|
|
407
|
+
maxDiscussions: {
|
|
408
|
+
type: 'number',
|
|
409
|
+
description:
|
|
410
|
+
'Maximum remote discussions to fetch when discussions is true (default 10)',
|
|
411
|
+
},
|
|
412
|
+
analyzer: {
|
|
413
|
+
type: 'boolean',
|
|
414
|
+
description:
|
|
415
|
+
'Query local @plansmithai/analyzer SQLite database (.plansmith/analyzer.sqlite) for institutional memory citations',
|
|
416
|
+
},
|
|
417
|
+
analyzerDb: {
|
|
418
|
+
type: 'string',
|
|
419
|
+
description:
|
|
420
|
+
'Explicit path to @plansmithai/analyzer SQLite database file or directory',
|
|
421
|
+
},
|
|
422
|
+
repos: {
|
|
423
|
+
type: 'array',
|
|
424
|
+
items: { type: 'string' },
|
|
425
|
+
description:
|
|
426
|
+
'Additional repository roots to include in multi-repository history explanation and cross-repo ADR/doc/test discovery',
|
|
427
|
+
},
|
|
299
428
|
},
|
|
300
429
|
required: ['path', 'target'],
|
|
301
430
|
},
|
|
@@ -349,18 +478,102 @@ class MCPServer {
|
|
|
349
478
|
type: 'string',
|
|
350
479
|
description: "Count with this model's tokenizer encoding and report the fit",
|
|
351
480
|
},
|
|
481
|
+
repos: {
|
|
482
|
+
type: 'array',
|
|
483
|
+
items: { type: 'string' },
|
|
484
|
+
description:
|
|
485
|
+
'Additional repository roots to pack into one workspace document under one shared token budget',
|
|
486
|
+
},
|
|
352
487
|
},
|
|
353
488
|
required: ['path'],
|
|
354
489
|
},
|
|
355
490
|
},
|
|
491
|
+
{
|
|
492
|
+
name: 'focus_context',
|
|
493
|
+
description:
|
|
494
|
+
'Focused neighborhood context around a target file (or file:symbol): packs the target file together with its outgoing imports/callees and incoming importers/callers up to `depth` hops in the repository import and AST symbol call graph (directional traversal avoids shared-hub explosion), honouring .gitignore/.contextignore and export policy, with secrets redacted and trimmed to a token budget (target file kept first).',
|
|
495
|
+
inputSchema: {
|
|
496
|
+
type: 'object',
|
|
497
|
+
properties: {
|
|
498
|
+
path: { type: 'string', description: 'Project root' },
|
|
499
|
+
file: {
|
|
500
|
+
type: 'string',
|
|
501
|
+
description:
|
|
502
|
+
'Project-relative (or <alias>/<path>) file or file:symbol to focus on (e.g. "src/auth/login.ts" or "src/auth/login.ts:authenticate")',
|
|
503
|
+
},
|
|
504
|
+
symbol: {
|
|
505
|
+
type: 'string',
|
|
506
|
+
description:
|
|
507
|
+
'Optional function or method name inside `file` to traverse the cross-file AST symbol call graph around',
|
|
508
|
+
},
|
|
509
|
+
depth: {
|
|
510
|
+
type: 'number',
|
|
511
|
+
description: `Import/call graph hops (default ${DEFAULT_FOCUS_DEPTH}; 0 = target file only)`,
|
|
512
|
+
},
|
|
513
|
+
direction: {
|
|
514
|
+
type: 'string',
|
|
515
|
+
enum: ['both', 'imports', 'importers', 'calls', 'callers'],
|
|
516
|
+
default: 'both',
|
|
517
|
+
},
|
|
518
|
+
callGraph: {
|
|
519
|
+
type: 'boolean',
|
|
520
|
+
description:
|
|
521
|
+
'Include cross-file AST symbol call graph edges alongside module imports (default true)',
|
|
522
|
+
},
|
|
523
|
+
includeTests: {
|
|
524
|
+
type: 'boolean',
|
|
525
|
+
description: 'Include test files in the neighborhood (default true)',
|
|
526
|
+
},
|
|
527
|
+
style: {
|
|
528
|
+
type: 'string',
|
|
529
|
+
enum: ['xml', 'markdown', 'plain', 'json'],
|
|
530
|
+
default: 'xml',
|
|
531
|
+
},
|
|
532
|
+
compress: {
|
|
533
|
+
type: 'boolean',
|
|
534
|
+
description: 'Keep signatures, replace function bodies with a placeholder',
|
|
535
|
+
},
|
|
536
|
+
lineNumbers: { type: 'boolean', description: 'Number every line' },
|
|
537
|
+
tree: {
|
|
538
|
+
type: 'boolean',
|
|
539
|
+
description: 'Directory tree at the top (default true)',
|
|
540
|
+
},
|
|
541
|
+
instructions: {
|
|
542
|
+
type: 'string',
|
|
543
|
+
description: 'Text placed at the end, after the files',
|
|
544
|
+
},
|
|
545
|
+
budget: {
|
|
546
|
+
type: 'number',
|
|
547
|
+
description: `Maximum tokens of the document (default ${TOOL_DEFAULT_BUDGET})`,
|
|
548
|
+
},
|
|
549
|
+
targetModel: {
|
|
550
|
+
type: 'string',
|
|
551
|
+
description: "Count with this model's tokenizer encoding and report the fit",
|
|
552
|
+
},
|
|
553
|
+
repos: {
|
|
554
|
+
type: 'array',
|
|
555
|
+
items: { type: 'string' },
|
|
556
|
+
description:
|
|
557
|
+
'Additional repository roots to include in the workspace import/call graph neighborhood under one shared token budget',
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
required: ['path', 'file'],
|
|
561
|
+
},
|
|
562
|
+
},
|
|
356
563
|
{
|
|
357
564
|
name: 'check_policy',
|
|
358
565
|
description:
|
|
359
|
-
'The effective export policy of a project (.ctxman/policy.json plus the organisation policy named by CTXMAN_POLICY): denied paths, redaction and token limits, allowed target models, AI-tool ignore files and audit logging. Reports whether the policy files are valid and, if not, every error.',
|
|
566
|
+
'The effective export policy of a project or multi-repository workspace (.ctxman/policy.json plus the organisation policy named by CTXMAN_POLICY): denied paths, redaction and token limits, allowed target models, AI-tool ignore files and audit logging. Reports whether the policy files are valid and, if not, every error.',
|
|
360
567
|
inputSchema: {
|
|
361
568
|
type: 'object',
|
|
362
569
|
properties: {
|
|
363
570
|
path: { type: 'string', description: 'Project root' },
|
|
571
|
+
repos: {
|
|
572
|
+
type: 'array',
|
|
573
|
+
items: { type: 'string' },
|
|
574
|
+
description:
|
|
575
|
+
'Additional repository roots to validate and summarize together in a multi-repository workspace',
|
|
576
|
+
},
|
|
364
577
|
},
|
|
365
578
|
required: ['path'],
|
|
366
579
|
},
|
|
@@ -368,7 +581,7 @@ class MCPServer {
|
|
|
368
581
|
{
|
|
369
582
|
name: 'audit_log',
|
|
370
583
|
description:
|
|
371
|
-
"Exports recorded in the project's audit log while the export policy enables audit: when, by whom, which command, where to, which files, token count and a SHA-256 of the exported text. Newest last.",
|
|
584
|
+
"Exports recorded in the project's (or multi-repository workspace's) audit log while the export policy enables audit: when, by whom, which command, where to, which files, token count and a SHA-256 of the exported text. Newest last.",
|
|
372
585
|
inputSchema: {
|
|
373
586
|
type: 'object',
|
|
374
587
|
properties: {
|
|
@@ -382,6 +595,12 @@ class MCPServer {
|
|
|
382
595
|
type: 'number',
|
|
383
596
|
description: `The last N events (default ${DEFAULT_AUDIT_LIMIT}; 0 = all)`,
|
|
384
597
|
},
|
|
598
|
+
repos: {
|
|
599
|
+
type: 'array',
|
|
600
|
+
items: { type: 'string' },
|
|
601
|
+
description:
|
|
602
|
+
'Additional repository roots whose export audit logs should be merged chronologically with repo attribution',
|
|
603
|
+
},
|
|
385
604
|
},
|
|
386
605
|
required: ['path'],
|
|
387
606
|
},
|
|
@@ -389,7 +608,7 @@ class MCPServer {
|
|
|
389
608
|
{
|
|
390
609
|
name: 'ask_codebase',
|
|
391
610
|
description:
|
|
392
|
-
'Semantic search: the files whose content is closest in meaning to a question, from a local embedding index (.ctxman/self-knowledge
|
|
611
|
+
'Semantic search: the files whose content is closest in meaning to a question, from a local SQLite embedding index (.ctxman/self-knowledge/index.sqlite via built-in node:sqlite, single-repo or multi-repo workspace). Uses @xenova/transformers when installed and automatically falls back to deterministic offline LocalHashEmbeddingProvider otherwise. For exact names or keywords, search_code is faster.',
|
|
393
612
|
inputSchema: {
|
|
394
613
|
type: 'object',
|
|
395
614
|
properties: {
|
|
@@ -399,10 +618,152 @@ class MCPServer {
|
|
|
399
618
|
type: 'number',
|
|
400
619
|
description: `How many files (default ${DEFAULT_ASK_LIMIT})`,
|
|
401
620
|
},
|
|
621
|
+
repos: {
|
|
622
|
+
type: 'array',
|
|
623
|
+
items: { type: 'string' },
|
|
624
|
+
description:
|
|
625
|
+
'Additional repository roots to include in multi-repository semantic search',
|
|
626
|
+
},
|
|
402
627
|
},
|
|
403
628
|
required: ['path', 'query'],
|
|
404
629
|
},
|
|
405
630
|
},
|
|
631
|
+
{
|
|
632
|
+
name: 'task_context',
|
|
633
|
+
description:
|
|
634
|
+
'Task-driven automatic context builder: fuses hybrid search, PageRank import/call neighborhood, and architectural map for a task description within a token budget (single-repo or multi-repo workspace).',
|
|
635
|
+
inputSchema: {
|
|
636
|
+
type: 'object',
|
|
637
|
+
properties: {
|
|
638
|
+
path: { type: 'string', description: 'Project root' },
|
|
639
|
+
task: { type: 'string', description: 'Natural-language task or issue description' },
|
|
640
|
+
budget: {
|
|
641
|
+
type: 'number',
|
|
642
|
+
description: `Maximum tokens (default ${DEFAULT_TASK_BUDGET})`,
|
|
643
|
+
},
|
|
644
|
+
format: { type: 'string', enum: ['xml', 'markdown', 'json'], default: 'xml' },
|
|
645
|
+
model: { type: 'string', description: 'Optional target LLM model ID' },
|
|
646
|
+
callGraph: {
|
|
647
|
+
type: 'boolean',
|
|
648
|
+
description:
|
|
649
|
+
'Include cross-file AST symbol call graph edges alongside module imports (default true)',
|
|
650
|
+
},
|
|
651
|
+
repos: {
|
|
652
|
+
type: 'array',
|
|
653
|
+
items: { type: 'string' },
|
|
654
|
+
description:
|
|
655
|
+
'Additional repository roots to include in a multi-repository task context bundle under one shared token budget',
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
required: ['path', 'task'],
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
name: 'diagnose_failure',
|
|
663
|
+
description:
|
|
664
|
+
'Diagnose a stack trace, test failure, or error log: parses implicated repo files/lines (single-repo or multi-repo workspace), packs enclosing symbols and 1-hop graph neighbors, and surfaces recent commits that touched them.',
|
|
665
|
+
inputSchema: {
|
|
666
|
+
type: 'object',
|
|
667
|
+
properties: {
|
|
668
|
+
path: { type: 'string', description: 'Project root' },
|
|
669
|
+
trace: { type: 'string', description: 'Error stack trace or failure log text' },
|
|
670
|
+
budget: {
|
|
671
|
+
type: 'number',
|
|
672
|
+
description: `Maximum tokens (default ${DEFAULT_DIAGNOSE_BUDGET})`,
|
|
673
|
+
},
|
|
674
|
+
format: { type: 'string', enum: ['xml', 'markdown', 'json'], default: 'xml' },
|
|
675
|
+
callGraph: {
|
|
676
|
+
type: 'boolean',
|
|
677
|
+
description:
|
|
678
|
+
'Include cross-file AST symbol call graph edges alongside module imports (default true)',
|
|
679
|
+
},
|
|
680
|
+
repos: {
|
|
681
|
+
type: 'array',
|
|
682
|
+
items: { type: 'string' },
|
|
683
|
+
description:
|
|
684
|
+
'Additional repository roots to resolve stack frames and 1-hop neighbors across under one shared token budget',
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
required: ['path', 'trace'],
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
name: 'find_experts',
|
|
692
|
+
description:
|
|
693
|
+
'Expert Finder ("Who knows about this?"): ranks code and domain experts for a file, symbol, directory, or natural-language topic (single-repo or multi-repo workspace) by fusing surviving git blame ownership, recency-decayed commit/churn history, and CODEOWNERS stewardship.',
|
|
694
|
+
inputSchema: {
|
|
695
|
+
type: 'object',
|
|
696
|
+
properties: {
|
|
697
|
+
path: { type: 'string', description: 'Project root inside a git repository' },
|
|
698
|
+
target: {
|
|
699
|
+
type: 'string',
|
|
700
|
+
description:
|
|
701
|
+
'File path (<alias>/<path>), file:symbol, file#L10-L40, directory, or natural-language topic query',
|
|
702
|
+
},
|
|
703
|
+
limit: {
|
|
704
|
+
type: 'number',
|
|
705
|
+
description: `Maximum ranked experts to return (default ${DEFAULT_WHO_LIMIT})`,
|
|
706
|
+
},
|
|
707
|
+
halfLifeDays: {
|
|
708
|
+
type: 'number',
|
|
709
|
+
description: 'Recency exponential decay half-life in days (default 90)',
|
|
710
|
+
},
|
|
711
|
+
style: {
|
|
712
|
+
type: 'string',
|
|
713
|
+
enum: ['markdown', 'xml', 'json'],
|
|
714
|
+
default: 'markdown',
|
|
715
|
+
},
|
|
716
|
+
repos: {
|
|
717
|
+
type: 'array',
|
|
718
|
+
items: { type: 'string' },
|
|
719
|
+
description:
|
|
720
|
+
'Additional repository roots inside git repositories to include in multi-repository expert ranking',
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
required: ['path', 'target'],
|
|
724
|
+
},
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
name: 'query_knowledge_graph',
|
|
728
|
+
description:
|
|
729
|
+
'Query the persistent SQLite knowledge graph (.ctxman/self-knowledge/index.sqlite) or IncrementalAnalyzer cache for file nodes, method declarations, and import/dependency edges.',
|
|
730
|
+
inputSchema: {
|
|
731
|
+
type: 'object',
|
|
732
|
+
properties: {
|
|
733
|
+
path: { type: 'string', description: 'Project root' },
|
|
734
|
+
query: {
|
|
735
|
+
type: 'string',
|
|
736
|
+
description:
|
|
737
|
+
'Optional substring or symbol query to filter graph nodes and method declarations',
|
|
738
|
+
},
|
|
739
|
+
file: {
|
|
740
|
+
type: 'string',
|
|
741
|
+
description:
|
|
742
|
+
'Optional project-relative file path to inspect along with its dependency neighborhood',
|
|
743
|
+
},
|
|
744
|
+
depth: {
|
|
745
|
+
type: 'number',
|
|
746
|
+
description: 'Neighborhood hop depth when file is provided (default: 1, max: 4)',
|
|
747
|
+
},
|
|
748
|
+
limit: {
|
|
749
|
+
type: 'number',
|
|
750
|
+
description: `Maximum number of graph nodes to return (default ${DEFAULT_GRAPH_LIMIT})`,
|
|
751
|
+
},
|
|
752
|
+
refresh: {
|
|
753
|
+
type: 'boolean',
|
|
754
|
+
description:
|
|
755
|
+
'Re-synchronize the IncrementalAnalyzer cache and SQLite knowledge graph before querying',
|
|
756
|
+
},
|
|
757
|
+
repos: {
|
|
758
|
+
type: 'array',
|
|
759
|
+
items: { type: 'string' },
|
|
760
|
+
description:
|
|
761
|
+
'Optional additional repository roots to include in a multi-repository knowledge graph under <alias>/<path>',
|
|
762
|
+
},
|
|
763
|
+
},
|
|
764
|
+
required: ['path'],
|
|
765
|
+
},
|
|
766
|
+
},
|
|
406
767
|
],
|
|
407
768
|
};
|
|
408
769
|
});
|
|
@@ -468,6 +829,21 @@ class MCPServer {
|
|
|
468
829
|
}),
|
|
469
830
|
};
|
|
470
831
|
}
|
|
832
|
+
if (args.repos !== undefined) {
|
|
833
|
+
if (!Array.isArray(args.repos)) {
|
|
834
|
+
throw new Error('repos must be an array of directory paths');
|
|
835
|
+
}
|
|
836
|
+
args = {
|
|
837
|
+
...args,
|
|
838
|
+
repos: args.repos.map((repoPath) =>
|
|
839
|
+
resolveSandboxPath(repoPath, {
|
|
840
|
+
root: this.root,
|
|
841
|
+
expect: 'directory',
|
|
842
|
+
name: 'repos',
|
|
843
|
+
})
|
|
844
|
+
),
|
|
845
|
+
};
|
|
846
|
+
}
|
|
471
847
|
|
|
472
848
|
switch (name) {
|
|
473
849
|
case 'analyze_codebase': {
|
|
@@ -476,6 +852,28 @@ class MCPServer {
|
|
|
476
852
|
// Update resource provider project path
|
|
477
853
|
this.resourceProvider.projectPath = projectPath;
|
|
478
854
|
|
|
855
|
+
if (args.repos?.length > 0) {
|
|
856
|
+
const resolvedRepos = resolveWorkspaceRepos([projectPath, ...args.repos]);
|
|
857
|
+
const allFiles = [];
|
|
858
|
+
for (const repo of resolvedRepos) {
|
|
859
|
+
const scanner = new Scanner(repo.root, { policy: repo.policy });
|
|
860
|
+
for (const file of scanner.scan()) {
|
|
861
|
+
const relPosix = `${repo.alias}/${file.relativePath.split(path.sep).join('/')}`;
|
|
862
|
+
allFiles.push({
|
|
863
|
+
...file,
|
|
864
|
+
relativePath: relPosix,
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
const analysis = await this.analyzer.analyze(allFiles);
|
|
869
|
+
analysis.repos = resolvedRepos.map((r) => ({ alias: r.alias, root: r.root }));
|
|
870
|
+
const analysisId = Date.now().toString();
|
|
871
|
+
this.resourceProvider.cacheAnalysis(analysisId, analysis);
|
|
872
|
+
return {
|
|
873
|
+
content: [{ type: 'text', text: JSON.stringify(analysis, null, 2) }],
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
|
|
479
877
|
const scanner = new Scanner(projectPath);
|
|
480
878
|
const files = scanner.scan();
|
|
481
879
|
const analysis = await this.analyzer.analyze(files);
|
|
@@ -499,20 +897,57 @@ class MCPServer {
|
|
|
499
897
|
// Update resource provider project path
|
|
500
898
|
this.resourceProvider.projectPath = projectPath;
|
|
501
899
|
|
|
502
|
-
const scanner = new Scanner(projectPath);
|
|
503
|
-
const files = scanner.scan();
|
|
504
|
-
const analysis = await this.analyzer.analyze(files);
|
|
505
|
-
|
|
506
|
-
// Configure ContextBuilder based on args
|
|
507
900
|
const builderOptions = {
|
|
508
901
|
targetTokens: args.maxTokens,
|
|
509
902
|
useCase: args.template || 'custom',
|
|
510
903
|
};
|
|
511
904
|
const contextBuilder = new ContextBuilder(builderOptions);
|
|
905
|
+
const templateName = args.template || 'custom';
|
|
906
|
+
|
|
907
|
+
if (args.repos?.length > 0) {
|
|
908
|
+
const resolvedRepos = resolveWorkspaceRepos([projectPath, ...args.repos]);
|
|
909
|
+
const allFiles = [];
|
|
910
|
+
for (const repo of resolvedRepos) {
|
|
911
|
+
const scanner = new Scanner(repo.root, { policy: repo.policy });
|
|
912
|
+
for (const file of scanner.scan()) {
|
|
913
|
+
const relPosix = `${repo.alias}/${file.relativePath.split(path.sep).join('/')}`;
|
|
914
|
+
allFiles.push({
|
|
915
|
+
...file,
|
|
916
|
+
relativePath: relPosix,
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
const analysis = await this.analyzer.analyze(allFiles);
|
|
921
|
+
const context = contextBuilder.build(analysis);
|
|
922
|
+
context.repos = resolvedRepos.map((r) => ({ alias: r.alias, root: r.root }));
|
|
923
|
+
const contextText = JSON.stringify(context, null, 2);
|
|
924
|
+
const exportedFiles = Object.values(context.files || {}).flat();
|
|
925
|
+
for (const repo of resolvedRepos) {
|
|
926
|
+
const prefix = `${repo.alias}/`;
|
|
927
|
+
const repoFiles = exportedFiles
|
|
928
|
+
.filter((f) => typeof f.path === 'string' && f.path.startsWith(prefix))
|
|
929
|
+
.map((f) => ({ ...f, path: f.path.slice(prefix.length) }));
|
|
930
|
+
guardExport(repo.policy, {
|
|
931
|
+
command: 'mcp generate_context',
|
|
932
|
+
root: repo.root,
|
|
933
|
+
destination: 'mcp',
|
|
934
|
+
format: 'json',
|
|
935
|
+
files: repoFiles,
|
|
936
|
+
text: contextText,
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
this.resourceProvider.cacheContext(templateName, contextText);
|
|
940
|
+
return {
|
|
941
|
+
content: [{ type: 'text', text: contextText }],
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const scanner = new Scanner(projectPath);
|
|
946
|
+
const files = scanner.scan();
|
|
947
|
+
const analysis = await this.analyzer.analyze(files);
|
|
512
948
|
const context = contextBuilder.build(analysis);
|
|
513
949
|
|
|
514
950
|
// Cache context as a resource
|
|
515
|
-
const templateName = args.template || 'custom';
|
|
516
951
|
const contextText = JSON.stringify(context, null, 2);
|
|
517
952
|
// Export policy: throws PolicyViolationError (an isError result) and logs the export
|
|
518
953
|
guardExport(loadPolicy(projectPath), {
|
|
@@ -536,7 +971,6 @@ class MCPServer {
|
|
|
536
971
|
}
|
|
537
972
|
case 'git_diff': {
|
|
538
973
|
const projectPath = args.path;
|
|
539
|
-
const gitClient = new GitClient(projectPath);
|
|
540
974
|
const branch = args.branch || 'main';
|
|
541
975
|
|
|
542
976
|
if (!isValidGitRef(branch)) {
|
|
@@ -548,6 +982,47 @@ class MCPServer {
|
|
|
548
982
|
};
|
|
549
983
|
}
|
|
550
984
|
|
|
985
|
+
if (args.repos?.length > 0) {
|
|
986
|
+
const resolvedRepos = resolveWorkspaceRepos([projectPath, ...args.repos]);
|
|
987
|
+
const diffs = [];
|
|
988
|
+
const errors = [];
|
|
989
|
+
for (const repo of resolvedRepos) {
|
|
990
|
+
try {
|
|
991
|
+
const gitClient = new GitClient(repo.root);
|
|
992
|
+
const rawDiff = gitClient.exec([
|
|
993
|
+
'diff',
|
|
994
|
+
branch,
|
|
995
|
+
'--',
|
|
996
|
+
...(this.root ? ['.'] : []),
|
|
997
|
+
]);
|
|
998
|
+
if (rawDiff && rawDiff.trim()) {
|
|
999
|
+
const prefixed = rawDiff
|
|
1000
|
+
.replace(
|
|
1001
|
+
/^diff --git a\/(.+) b\/(.+)$/gm,
|
|
1002
|
+
`diff --git a/${repo.alias}/$1 b/${repo.alias}/$2`
|
|
1003
|
+
)
|
|
1004
|
+
.replace(/^--- a\/(.+)$/gm, `--- a/${repo.alias}/$1`)
|
|
1005
|
+
.replace(/^\+\+\+ b\/(.+)$/gm, `+++ b/${repo.alias}/$1`);
|
|
1006
|
+
diffs.push(prefixed.trimEnd());
|
|
1007
|
+
}
|
|
1008
|
+
} catch (e) {
|
|
1009
|
+
errors.push(`${repo.alias}: ${e.message}`);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
if (errors.length === resolvedRepos.length) {
|
|
1013
|
+
return {
|
|
1014
|
+
content: [{ type: 'text', text: `Error executing git diff: ${errors[0]}` }],
|
|
1015
|
+
isError: true,
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
const notes = errors.map((err) => `# Note: ${err}`).join('\n');
|
|
1019
|
+
const combined = [diffs.join('\n'), notes].filter(Boolean).join('\n\n');
|
|
1020
|
+
return {
|
|
1021
|
+
content: [{ type: 'text', text: combined || 'No changes detected.' }],
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const gitClient = new GitClient(projectPath);
|
|
551
1026
|
let diff;
|
|
552
1027
|
try {
|
|
553
1028
|
// With a root, keep the diff to that directory even if the repository is larger
|
|
@@ -565,6 +1040,17 @@ class MCPServer {
|
|
|
565
1040
|
}
|
|
566
1041
|
case 'search_code': {
|
|
567
1042
|
const { path: projectPath, query, type = 'regex' } = args;
|
|
1043
|
+
if (
|
|
1044
|
+
args.repos?.length > 0 &&
|
|
1045
|
+
(type === 'ranked' || type === 'hybrid' || type === 'semantic')
|
|
1046
|
+
) {
|
|
1047
|
+
const result = searchWorkspace([projectPath, ...args.repos], query, {
|
|
1048
|
+
limit: args.limit ?? DEFAULT_LIMIT,
|
|
1049
|
+
maxTokens: args.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
1050
|
+
redact: args.redact !== false,
|
|
1051
|
+
});
|
|
1052
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1053
|
+
}
|
|
568
1054
|
if (type === 'ranked') {
|
|
569
1055
|
const result = searchProject(projectPath, query, {
|
|
570
1056
|
limit: args.limit ?? DEFAULT_LIMIT,
|
|
@@ -573,6 +1059,16 @@ class MCPServer {
|
|
|
573
1059
|
});
|
|
574
1060
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
575
1061
|
}
|
|
1062
|
+
if (type === 'hybrid' || type === 'semantic') {
|
|
1063
|
+
const result = await hybridSearch(projectPath, query, {
|
|
1064
|
+
mode: type,
|
|
1065
|
+
fallbackToBm25: true,
|
|
1066
|
+
limit: args.limit ?? DEFAULT_LIMIT,
|
|
1067
|
+
maxTokens: args.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
1068
|
+
redact: args.redact !== false,
|
|
1069
|
+
});
|
|
1070
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1071
|
+
}
|
|
576
1072
|
let pattern = null;
|
|
577
1073
|
if (type === 'regex') {
|
|
578
1074
|
try {
|
|
@@ -584,6 +1080,37 @@ class MCPServer {
|
|
|
584
1080
|
};
|
|
585
1081
|
}
|
|
586
1082
|
}
|
|
1083
|
+
|
|
1084
|
+
if (args.repos?.length > 0) {
|
|
1085
|
+
const resolvedRepos = resolveWorkspaceRepos([projectPath, ...args.repos]);
|
|
1086
|
+
const results = [];
|
|
1087
|
+
for (const repo of resolvedRepos) {
|
|
1088
|
+
const scanner = new Scanner(repo.root, { policy: repo.policy });
|
|
1089
|
+
for (const file of scanner.scan()) {
|
|
1090
|
+
try {
|
|
1091
|
+
const content = fs.readFileSync(file.path, 'utf-8');
|
|
1092
|
+
const lines = content.split('\n');
|
|
1093
|
+
const relPosix = `${repo.alias}/${file.relativePath.split(path.sep).join('/')}`;
|
|
1094
|
+
lines.forEach((line, index) => {
|
|
1095
|
+
const match = pattern ? pattern.test(line) : line.includes(query);
|
|
1096
|
+
if (match) {
|
|
1097
|
+
results.push({
|
|
1098
|
+
file: relPosix,
|
|
1099
|
+
line: index + 1,
|
|
1100
|
+
content: line.trim(),
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
} catch (_e) {
|
|
1105
|
+
// Skip read errors
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
return {
|
|
1110
|
+
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
|
|
587
1114
|
const scanner = new Scanner(projectPath);
|
|
588
1115
|
const files = scanner.scan();
|
|
589
1116
|
const results = [];
|
|
@@ -621,6 +1148,48 @@ class MCPServer {
|
|
|
621
1148
|
file = path.basename(projectPath);
|
|
622
1149
|
projectPath = path.dirname(projectPath);
|
|
623
1150
|
}
|
|
1151
|
+
|
|
1152
|
+
if (args.repos?.length > 0) {
|
|
1153
|
+
const resolvedRepos = resolveWorkspaceRepos([projectPath, ...args.repos]);
|
|
1154
|
+
const targetPosix = file ? file.split(path.sep).join('/').replace(/^\.\//, '') : null;
|
|
1155
|
+
const allFiles = [];
|
|
1156
|
+
for (const repo of resolvedRepos) {
|
|
1157
|
+
const scanner = new Scanner(repo.root, { policy: repo.policy });
|
|
1158
|
+
for (const f of scanner.scan()) {
|
|
1159
|
+
const barePosix = f.relativePath.split(path.sep).join('/');
|
|
1160
|
+
const relPosix = `${repo.alias}/${barePosix}`;
|
|
1161
|
+
if (!targetPosix || relPosix === targetPosix || barePosix === targetPosix) {
|
|
1162
|
+
allFiles.push({
|
|
1163
|
+
...f,
|
|
1164
|
+
relativePath: relPosix,
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
if (targetPosix && allFiles.length === 0) {
|
|
1170
|
+
return {
|
|
1171
|
+
content: [{ type: 'text', text: `File not found in project: ${file}` }],
|
|
1172
|
+
isError: true,
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
const methodAnalyzer = new Analyzer({ methodLevel: true, cache: true });
|
|
1176
|
+
const analysis = await methodAnalyzer.analyze(allFiles);
|
|
1177
|
+
const methods = [];
|
|
1178
|
+
if (analysis.files) {
|
|
1179
|
+
for (const analyzedFile of analysis.files) {
|
|
1180
|
+
if (analyzedFile.methods && analyzedFile.methods.length > 0) {
|
|
1181
|
+
methods.push({
|
|
1182
|
+
file: analyzedFile.relativePath,
|
|
1183
|
+
methods: analyzedFile.methods,
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
return {
|
|
1189
|
+
content: [{ type: 'text', text: JSON.stringify(methods, null, 2) }],
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
|
|
624
1193
|
const scanner = new Scanner(projectPath);
|
|
625
1194
|
// Optional `file`: one project-relative file instead of the whole project
|
|
626
1195
|
const target = file ? path.normalize(file) : null;
|
|
@@ -635,7 +1204,7 @@ class MCPServer {
|
|
|
635
1204
|
}
|
|
636
1205
|
|
|
637
1206
|
// Use Analyzer with methodLevel: true
|
|
638
|
-
const methodAnalyzer = new Analyzer({ methodLevel: true });
|
|
1207
|
+
const methodAnalyzer = new Analyzer({ methodLevel: true, cache: true });
|
|
639
1208
|
const analysis = await methodAnalyzer.analyze(files);
|
|
640
1209
|
|
|
641
1210
|
const methods = [];
|
|
@@ -655,10 +1224,18 @@ class MCPServer {
|
|
|
655
1224
|
};
|
|
656
1225
|
}
|
|
657
1226
|
case 'repo_map': {
|
|
658
|
-
const map =
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
1227
|
+
const map =
|
|
1228
|
+
args.repos?.length > 0
|
|
1229
|
+
? mapWorkspace([args.path, ...args.repos], {
|
|
1230
|
+
tokens: args.tokens,
|
|
1231
|
+
focus: args.focus,
|
|
1232
|
+
callGraph: args.callGraph,
|
|
1233
|
+
})
|
|
1234
|
+
: renderRepoMap(args.path, {
|
|
1235
|
+
tokens: args.tokens,
|
|
1236
|
+
focus: args.focus,
|
|
1237
|
+
callGraph: args.callGraph,
|
|
1238
|
+
});
|
|
662
1239
|
const notes = map.warnings.map((warning) => `Note: ${warning}`).join('\n');
|
|
663
1240
|
return {
|
|
664
1241
|
content: [
|
|
@@ -671,6 +1248,7 @@ class MCPServer {
|
|
|
671
1248
|
}
|
|
672
1249
|
case 'pr_context': {
|
|
673
1250
|
const result = await buildPrContext(args.path, {
|
|
1251
|
+
repos: args.repos,
|
|
674
1252
|
base: args.base,
|
|
675
1253
|
head: args.head,
|
|
676
1254
|
budget: args.budget == null ? undefined : Math.floor(args.budget),
|
|
@@ -682,6 +1260,7 @@ class MCPServer {
|
|
|
682
1260
|
const result = getSymbol(args.path, args.name, {
|
|
683
1261
|
file: args.file,
|
|
684
1262
|
redact: args.redact !== false,
|
|
1263
|
+
repos: args.repos,
|
|
685
1264
|
});
|
|
686
1265
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
687
1266
|
}
|
|
@@ -692,6 +1271,7 @@ class MCPServer {
|
|
|
692
1271
|
const { largestFiles, ...summary } = await countForModel(args.path, {
|
|
693
1272
|
model: args.model,
|
|
694
1273
|
text: args.text,
|
|
1274
|
+
repos: args.repos,
|
|
695
1275
|
});
|
|
696
1276
|
const top = Number.isInteger(args.top) && args.top >= 0 ? args.top : 10;
|
|
697
1277
|
const result = { ...summary, largestFiles: largestFiles.slice(0, top) };
|
|
@@ -699,15 +1279,47 @@ class MCPServer {
|
|
|
699
1279
|
}
|
|
700
1280
|
case 'explain_history': {
|
|
701
1281
|
const whole = (value) => (typeof value === 'number' ? Math.floor(value) : value);
|
|
1282
|
+
const analyzerDb =
|
|
1283
|
+
args.analyzerDb !== undefined
|
|
1284
|
+
? resolveSandboxPath(args.analyzerDb, {
|
|
1285
|
+
root: this.root,
|
|
1286
|
+
expect: 'any',
|
|
1287
|
+
name: 'analyzerDb',
|
|
1288
|
+
})
|
|
1289
|
+
: undefined;
|
|
702
1290
|
const result = await explainHistory(args.path, args.target, {
|
|
1291
|
+
repos: args.repos,
|
|
703
1292
|
limit: whole(args.limit),
|
|
704
1293
|
budget: whole(args.budget ?? 20000),
|
|
705
1294
|
since: args.since,
|
|
706
1295
|
patches: args.patches === true,
|
|
1296
|
+
discussions: args.discussions === true,
|
|
1297
|
+
maxDiscussions: whole(args.maxDiscussions),
|
|
1298
|
+
analyzer: args.analyzer === true,
|
|
1299
|
+
analyzerDb,
|
|
707
1300
|
});
|
|
708
1301
|
return { content: [{ type: 'text', text: result.text }] };
|
|
709
1302
|
}
|
|
710
1303
|
case 'pack_context': {
|
|
1304
|
+
if (args.repos?.length > 0) {
|
|
1305
|
+
const result = await packWorkspace([args.path, ...args.repos], {
|
|
1306
|
+
include: args.include,
|
|
1307
|
+
exclude: args.exclude,
|
|
1308
|
+
style: args.style,
|
|
1309
|
+
compress: args.compress,
|
|
1310
|
+
lineNumbers: args.lineNumbers,
|
|
1311
|
+
tree: args.tree,
|
|
1312
|
+
instructions: args.instructions,
|
|
1313
|
+
budget: args.budget == null ? TOOL_DEFAULT_BUDGET : Math.floor(args.budget),
|
|
1314
|
+
targetModel: args.targetModel,
|
|
1315
|
+
});
|
|
1316
|
+
return {
|
|
1317
|
+
content: [
|
|
1318
|
+
{ type: 'text', text: result.text },
|
|
1319
|
+
{ type: 'text', text: describePack(result) },
|
|
1320
|
+
],
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
711
1323
|
const result = await packContext(args.path, {
|
|
712
1324
|
include: args.include,
|
|
713
1325
|
exclude: args.exclude,
|
|
@@ -729,7 +1341,35 @@ class MCPServer {
|
|
|
729
1341
|
],
|
|
730
1342
|
};
|
|
731
1343
|
}
|
|
1344
|
+
case 'focus_context': {
|
|
1345
|
+
const result = await focusContext(args.path, args.file ?? args.target, {
|
|
1346
|
+
repos: args.repos,
|
|
1347
|
+
symbol: args.symbol,
|
|
1348
|
+
depth: args.depth == null ? undefined : Math.floor(args.depth),
|
|
1349
|
+
direction: args.direction,
|
|
1350
|
+
callGraph: args.callGraph,
|
|
1351
|
+
includeTests: args.includeTests,
|
|
1352
|
+
cwd: args.path,
|
|
1353
|
+
style: args.style,
|
|
1354
|
+
compress: args.compress,
|
|
1355
|
+
lineNumbers: args.lineNumbers,
|
|
1356
|
+
tree: args.tree,
|
|
1357
|
+
instructions: args.instructions,
|
|
1358
|
+
budget: args.budget == null ? TOOL_DEFAULT_BUDGET : Math.floor(args.budget),
|
|
1359
|
+
targetModel: args.targetModel,
|
|
1360
|
+
});
|
|
1361
|
+
return {
|
|
1362
|
+
content: [
|
|
1363
|
+
{ type: 'text', text: result.text },
|
|
1364
|
+
{ type: 'text', text: describeFocus(result) },
|
|
1365
|
+
],
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
732
1368
|
case 'check_policy': {
|
|
1369
|
+
if (args.repos?.length > 0) {
|
|
1370
|
+
const result = checkWorkspacePolicies(args.path, args.repos);
|
|
1371
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1372
|
+
}
|
|
733
1373
|
let result;
|
|
734
1374
|
try {
|
|
735
1375
|
result = { valid: true, ...loadPolicy(args.path).toJSON() };
|
|
@@ -743,6 +1383,18 @@ class MCPServer {
|
|
|
743
1383
|
const since = args.since === undefined ? null : parseSince(String(args.since));
|
|
744
1384
|
const limit =
|
|
745
1385
|
Number.isInteger(args.limit) && args.limit >= 0 ? args.limit : DEFAULT_AUDIT_LIMIT;
|
|
1386
|
+
if (args.repos?.length > 0) {
|
|
1387
|
+
const ws = readWorkspaceAuditLogs(args.path, args.repos, { since, limit });
|
|
1388
|
+
const result = {
|
|
1389
|
+
repos: ws.repos,
|
|
1390
|
+
files: ws.files,
|
|
1391
|
+
total: ws.total,
|
|
1392
|
+
events: ws.events,
|
|
1393
|
+
skippedLines: ws.skippedLines,
|
|
1394
|
+
};
|
|
1395
|
+
if (ws.policyErrors.length > 0) result.policyErrors = ws.policyErrors;
|
|
1396
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1397
|
+
}
|
|
746
1398
|
// Only the policy's own logs: a tool argument never names a file to read
|
|
747
1399
|
const { files, error } = auditLogFiles(args.path);
|
|
748
1400
|
const { events, skipped, missing } = readAuditLog(files);
|
|
@@ -758,11 +1410,173 @@ class MCPServer {
|
|
|
758
1410
|
}
|
|
759
1411
|
case 'ask_codebase': {
|
|
760
1412
|
const result = await askCodebase(args.path, args.query, {
|
|
1413
|
+
repos: args.repos,
|
|
761
1414
|
limit: Number.isInteger(args.limit) ? args.limit : undefined,
|
|
762
1415
|
maxChars: ASK_SNIPPET_CHARS,
|
|
763
1416
|
});
|
|
764
1417
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
765
1418
|
}
|
|
1419
|
+
case 'task_context': {
|
|
1420
|
+
const result = await buildTaskContext(
|
|
1421
|
+
args.path ?? args.dir ?? process.cwd(),
|
|
1422
|
+
args.task ?? args.query ?? '',
|
|
1423
|
+
{
|
|
1424
|
+
repos: args.repos,
|
|
1425
|
+
budget: args.budget == null ? DEFAULT_TASK_BUDGET : Math.floor(args.budget),
|
|
1426
|
+
format: args.format,
|
|
1427
|
+
model: args.model,
|
|
1428
|
+
callGraph: args.callGraph,
|
|
1429
|
+
}
|
|
1430
|
+
);
|
|
1431
|
+
return {
|
|
1432
|
+
content: [
|
|
1433
|
+
{ type: 'text', text: result.text },
|
|
1434
|
+
{
|
|
1435
|
+
type: 'text',
|
|
1436
|
+
text: `Task Context: ${result.files.length} file(s), ${result.totalTokens}/${result.budget} tokens (${result.format}).`,
|
|
1437
|
+
},
|
|
1438
|
+
],
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
case 'diagnose_failure': {
|
|
1442
|
+
const result = await diagnoseFailure(
|
|
1443
|
+
args.path ?? args.dir ?? process.cwd(),
|
|
1444
|
+
args.trace ?? args.log ?? '',
|
|
1445
|
+
{
|
|
1446
|
+
repos: args.repos,
|
|
1447
|
+
budget: args.budget == null ? DEFAULT_DIAGNOSE_BUDGET : Math.floor(args.budget),
|
|
1448
|
+
format: args.format,
|
|
1449
|
+
callGraph: args.callGraph,
|
|
1450
|
+
}
|
|
1451
|
+
);
|
|
1452
|
+
return {
|
|
1453
|
+
content: [
|
|
1454
|
+
{ type: 'text', text: result.text },
|
|
1455
|
+
{
|
|
1456
|
+
type: 'text',
|
|
1457
|
+
text: `Diagnosed: ${result.implicatedFiles.length} implicated file(s), ${result.totalTokens}/${result.budget} tokens.`,
|
|
1458
|
+
},
|
|
1459
|
+
],
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
case 'find_experts': {
|
|
1463
|
+
const result = await findExperts(
|
|
1464
|
+
args.path ?? process.cwd(),
|
|
1465
|
+
args.target ?? args.query ?? '',
|
|
1466
|
+
{
|
|
1467
|
+
repos: args.repos,
|
|
1468
|
+
limit: Number.isInteger(args.limit) ? args.limit : undefined,
|
|
1469
|
+
halfLifeDays: typeof args.halfLifeDays === 'number' ? args.halfLifeDays : undefined,
|
|
1470
|
+
style: args.style || 'markdown',
|
|
1471
|
+
}
|
|
1472
|
+
);
|
|
1473
|
+
return {
|
|
1474
|
+
content: [{ type: 'text', text: result.text }],
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
case 'query_knowledge_graph': {
|
|
1478
|
+
const projectPath = args.path ?? process.cwd();
|
|
1479
|
+
const extraRepos = Array.isArray(args.repos) ? args.repos : [];
|
|
1480
|
+
const scanner = new Scanner(projectPath);
|
|
1481
|
+
scanner.getPolicy();
|
|
1482
|
+
for (const repoPath of extraRepos) {
|
|
1483
|
+
new Scanner(repoPath).getPolicy();
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
const inc = this.getIncrementalAnalyzer(projectPath, extraRepos);
|
|
1487
|
+
await inc.ensureSyncReady(projectPath);
|
|
1488
|
+
const store = inc.vectorStore;
|
|
1489
|
+
const initialCounts = store.graphCounts();
|
|
1490
|
+
|
|
1491
|
+
let bootstrapped = false;
|
|
1492
|
+
if (args.refresh === true || initialCounts.nodes === 0) {
|
|
1493
|
+
await inc.bootstrapSync(projectPath, { repos: extraRepos });
|
|
1494
|
+
bootstrapped = true;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
const rawGraph = await inc.getGraph();
|
|
1498
|
+
let nodes = rawGraph.nodes.filter((n) => !inc.isPathDenied(n.path));
|
|
1499
|
+
let edges = rawGraph.edges.filter(
|
|
1500
|
+
(e) => !inc.isPathDenied(e.from) && !inc.isPathDenied(e.to)
|
|
1501
|
+
);
|
|
1502
|
+
|
|
1503
|
+
if (typeof args.file === 'string' && args.file.trim()) {
|
|
1504
|
+
const targetFile = args.file.trim().replace(/\\/g, '/').replace(/^\.\//, '');
|
|
1505
|
+
const maxDepth = Math.max(
|
|
1506
|
+
1,
|
|
1507
|
+
Math.min(4, Number.isFinite(args.depth) ? Math.floor(args.depth) : 1)
|
|
1508
|
+
);
|
|
1509
|
+
const visited = new Set([targetFile]);
|
|
1510
|
+
let frontier = new Set([targetFile]);
|
|
1511
|
+
|
|
1512
|
+
for (let hop = 0; hop < maxDepth && frontier.size > 0; hop++) {
|
|
1513
|
+
const nextFrontier = new Set();
|
|
1514
|
+
for (const edge of edges) {
|
|
1515
|
+
if (frontier.has(edge.from) && !visited.has(edge.to)) {
|
|
1516
|
+
visited.add(edge.to);
|
|
1517
|
+
nextFrontier.add(edge.to);
|
|
1518
|
+
}
|
|
1519
|
+
if (frontier.has(edge.to) && !visited.has(edge.from)) {
|
|
1520
|
+
visited.add(edge.from);
|
|
1521
|
+
nextFrontier.add(edge.from);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
frontier = nextFrontier;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
nodes = nodes.filter((n) => visited.has(n.path));
|
|
1528
|
+
edges = edges.filter((e) => visited.has(e.from) && visited.has(e.to));
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
if (typeof args.query === 'string' && args.query.trim()) {
|
|
1532
|
+
const needle = args.query.trim().toLowerCase();
|
|
1533
|
+
nodes = nodes.filter((n) => {
|
|
1534
|
+
if (n.path.toLowerCase().includes(needle)) return true;
|
|
1535
|
+
if (Array.isArray(n.methods)) {
|
|
1536
|
+
return n.methods.some((m) => {
|
|
1537
|
+
const name = typeof m === 'string' ? m : m?.name || '';
|
|
1538
|
+
const sig = typeof m === 'object' ? m?.signature || '' : '';
|
|
1539
|
+
return (
|
|
1540
|
+
name.toLowerCase().includes(needle) || sig.toLowerCase().includes(needle)
|
|
1541
|
+
);
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
return false;
|
|
1545
|
+
});
|
|
1546
|
+
const matchedPaths = new Set(nodes.map((n) => n.path));
|
|
1547
|
+
edges = edges.filter((e) => matchedPaths.has(e.from) || matchedPaths.has(e.to));
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
const limit = Math.max(
|
|
1551
|
+
1,
|
|
1552
|
+
Math.min(
|
|
1553
|
+
500,
|
|
1554
|
+
Number.isFinite(args.limit) ? Math.floor(args.limit) : DEFAULT_GRAPH_LIMIT
|
|
1555
|
+
)
|
|
1556
|
+
);
|
|
1557
|
+
const limitedNodes = nodes.slice(0, limit);
|
|
1558
|
+
const keptPaths = new Set(limitedNodes.map((n) => n.path));
|
|
1559
|
+
const matchingEdges = edges.filter((e) => keptPaths.has(e.from) || keptPaths.has(e.to));
|
|
1560
|
+
|
|
1561
|
+
const counts = store.graphCounts();
|
|
1562
|
+
const payload = {
|
|
1563
|
+
path: projectPath,
|
|
1564
|
+
bootstrapped,
|
|
1565
|
+
cached: inc.fileCache.size > 0 || inc.graphNodes.size > 0,
|
|
1566
|
+
stats: {
|
|
1567
|
+
totalNodes: counts.nodes,
|
|
1568
|
+
totalEdges: counts.edges,
|
|
1569
|
+
vectorDocuments: store.count(),
|
|
1570
|
+
cachedFiles: inc.fileCache.size,
|
|
1571
|
+
},
|
|
1572
|
+
nodes: limitedNodes,
|
|
1573
|
+
edges: matchingEdges,
|
|
1574
|
+
};
|
|
1575
|
+
|
|
1576
|
+
return {
|
|
1577
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
766
1580
|
default:
|
|
767
1581
|
throw new Error(`Unknown tool: ${name}`);
|
|
768
1582
|
}
|