@graphpilot-oss/graphpilot 0.0.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/CHANGELOG.md +72 -126
  2. package/README.md +290 -102
  3. package/dist/cli.js +41 -1
  4. package/dist/cli.js.map +1 -1
  5. package/dist/edges.js +22 -11
  6. package/dist/edges.js.map +1 -1
  7. package/dist/indexer.js +3 -3
  8. package/dist/indexer.js.map +1 -1
  9. package/dist/init.d.ts +28 -0
  10. package/dist/init.js +112 -0
  11. package/dist/init.js.map +1 -0
  12. package/dist/interactions.d.ts +5 -4
  13. package/dist/interactions.js +0 -0
  14. package/dist/interactions.js.map +1 -1
  15. package/dist/mcp.js +119 -90
  16. package/dist/mcp.js.map +1 -1
  17. package/dist/repo-resolve.d.ts +47 -0
  18. package/dist/repo-resolve.js +195 -0
  19. package/dist/repo-resolve.js.map +1 -0
  20. package/dist/storage.js +10 -1
  21. package/dist/storage.js.map +1 -1
  22. package/dist/symbols.js +26 -2
  23. package/dist/symbols.js.map +1 -1
  24. package/dist/validation.js +30 -4
  25. package/dist/validation.js.map +1 -1
  26. package/dist/validators.d.ts +1 -5
  27. package/dist/validators.js +0 -11
  28. package/dist/validators.js.map +1 -1
  29. package/dist/watcher.d.ts +10 -0
  30. package/dist/watcher.js +70 -7
  31. package/dist/watcher.js.map +1 -1
  32. package/examples/README.md +105 -0
  33. package/examples/claude-code/README.md +125 -0
  34. package/examples/claude-code/claude-routing.md +102 -0
  35. package/examples/claude-code/claude_config.json +8 -0
  36. package/examples/cline/.clinerules +39 -0
  37. package/examples/cline/README.md +104 -0
  38. package/examples/cline/cline_mcp_settings.json +10 -0
  39. package/examples/continue/.continuerules +39 -0
  40. package/examples/continue/README.md +98 -0
  41. package/examples/continue/config.json +13 -0
  42. package/examples/cursor/.cursorrules +39 -0
  43. package/examples/cursor/README.md +98 -0
  44. package/examples/cursor/mcp.json +11 -0
  45. package/examples/windsurf/.windsurfrules +39 -0
  46. package/examples/windsurf/README.md +85 -0
  47. package/examples/windsurf/mcp_config.json +8 -0
  48. package/package.json +14 -4
  49. package/.editorconfig +0 -15
  50. package/.github/CODEOWNERS +0 -22
  51. package/.github/FUNDING.yml +0 -1
  52. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
  53. package/.github/ISSUE_TEMPLATE/config.yml +0 -5
  54. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -23
  55. package/.github/PULL_REQUEST_TEMPLATE.md +0 -19
  56. package/.github/dependabot.yml +0 -15
  57. package/.github/workflows/ci.yml +0 -62
  58. package/.github/workflows/release.yml +0 -50
  59. package/.prettierignore +0 -19
  60. package/.prettierrc.json +0 -20
  61. package/CODE_OF_CONDUCT.md +0 -83
  62. package/CONTRIBUTING.md +0 -111
  63. package/bench/README.md +0 -544
  64. package/bench/results/agent-tier-2026-05-22.md +0 -28
  65. package/bench/results/agent-tier-summary.md +0 -44
  66. package/bench/results/baseline-tier-2026-05-22.md +0 -23
  67. package/bench/results/baseline.json +0 -810
  68. package/bench/results/baseline.md +0 -28
  69. package/bench/run-agent-tier-automated.ts +0 -234
  70. package/bench/run-agent-tier.md +0 -125
  71. package/bench/run-baseline-tier.ts +0 -200
  72. package/bench/run.ts +0 -210
  73. package/bench/runner-baseline.ts +0 -177
  74. package/bench/runner-graphpilot.ts +0 -131
  75. package/bench/score-agent-tier.ts +0 -191
  76. package/bench/score.ts +0 -59
  77. package/bench/tasks.ts +0 -236
  78. package/dist/provenance.d.ts +0 -74
  79. package/dist/provenance.js +0 -95
  80. package/dist/provenance.js.map +0 -1
  81. package/docs/architecture.md +0 -311
  82. package/docs/limitations.md +0 -156
  83. package/docs/mcp-setup.md +0 -231
  84. package/docs/quickstart.md +0 -202
  85. package/eslint.config.js +0 -148
  86. package/lefthook.yml +0 -81
  87. package/pnpm-workspace.yaml +0 -6
  88. package/scripts/smoke-stdio.mjs +0 -97
  89. package/src/cli.ts +0 -171
  90. package/src/edges.ts +0 -202
  91. package/src/git.ts +0 -255
  92. package/src/graph-schema.ts +0 -229
  93. package/src/impact.ts +0 -218
  94. package/src/indexer.ts +0 -152
  95. package/src/interactions.ts +0 -0
  96. package/src/mcp.ts +0 -652
  97. package/src/parser.ts +0 -138
  98. package/src/provenance.ts +0 -115
  99. package/src/query.ts +0 -148
  100. package/src/redact.ts +0 -122
  101. package/src/storage.ts +0 -115
  102. package/src/symbols.ts +0 -173
  103. package/src/validation.ts +0 -69
  104. package/src/validators.ts +0 -253
  105. package/src/watcher.ts +0 -383
  106. package/tests/edges.test.ts +0 -175
  107. package/tests/fixtures/sample.ts +0 -32
  108. package/tests/git.test.ts +0 -303
  109. package/tests/graph-schema.test.ts +0 -321
  110. package/tests/impact.test.ts +0 -454
  111. package/tests/interactions.test.ts +0 -180
  112. package/tests/lint-policy.test.ts +0 -106
  113. package/tests/mcp-stdio.test.ts +0 -171
  114. package/tests/mcp.test.ts +0 -335
  115. package/tests/parser.test.ts +0 -31
  116. package/tests/provenance.test.ts +0 -132
  117. package/tests/query.test.ts +0 -160
  118. package/tests/redact.test.ts +0 -167
  119. package/tests/security.test.ts +0 -144
  120. package/tests/symbols.test.ts +0 -78
  121. package/tests/validators.test.ts +0 -193
  122. package/tests/watcher.test.ts +0 -250
  123. package/tsconfig.json +0 -18
package/src/mcp.ts DELETED
@@ -1,652 +0,0 @@
1
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
- import { resolve } from 'node:path';
5
- import { GraphIndex } from './query.js';
6
- import { indexDirectory } from './indexer.js';
7
- import { loadGraph, saveGraph, repoIdFor, type Graph } from './storage.js';
8
- import { validateRootPath } from './validation.js';
9
- import {
10
- validateGpIndex,
11
- validateGpRecall,
12
- validateGpCallers,
13
- validateGpImpact,
14
- validateGpStats,
15
- type GpRecallArgs,
16
- type GpCallersArgs,
17
- type GpImpactArgs,
18
- type GpIndexArgs,
19
- type GpStatsArgs,
20
- } from './validators.js';
21
- import { withInteractionLog } from './interactions.js';
22
- import { analyzeImpact, type ImpactCaller, type ImpactResult } from './impact.js';
23
- import { getChangedFiles, resolveIndexRoot } from './git.js';
24
- import type { SymbolRecord } from './symbols.js';
25
- import type { CallEdge } from './edges.js';
26
-
27
- const SERVER_NAME = 'graphpilot';
28
- const SERVER_VERSION = '0.0.1';
29
-
30
- // ----------------------------------------------------------------------------
31
- // Per-process cache of loaded GraphIndex by absolute repo path.
32
- // ----------------------------------------------------------------------------
33
-
34
- const indexCache = new Map<string, GraphIndex>();
35
-
36
- function getOrLoadIndex(
37
- rawPath: string | undefined,
38
- ): { idx: GraphIndex; root: string } | { error: string; root: string } {
39
- // Re-root to the git worktree top so MCP tool calls from a subdir of a
40
- // worktree still resolve to the branch-level index. Outside git this is
41
- // a no-op.
42
- const { root } = resolveIndexRoot(rawPath ?? process.cwd());
43
- const cached = indexCache.get(root);
44
- if (cached) return { idx: cached, root };
45
-
46
- const graph = loadGraph(root);
47
- if (!graph) {
48
- return {
49
- root,
50
- error:
51
- `No GraphPilot index found for ${root}.\n` +
52
- `Ask the user to run \`graphpilot index ${rawPath ?? '.'}\` first, or ` +
53
- `call the gp_index tool to build one.`,
54
- };
55
- }
56
- const idx = new GraphIndex(graph);
57
- indexCache.set(root, idx);
58
- return { idx, root };
59
- }
60
-
61
- function invalidateCache(absRoot: string): void {
62
- indexCache.delete(absRoot);
63
- }
64
-
65
- // ----------------------------------------------------------------------------
66
- // Tool-output formatting helpers (terse, agent-friendly text).
67
- // ----------------------------------------------------------------------------
68
-
69
- /**
70
- * Render the short SHA suffix for an inline evidence anchor. Returns an
71
- * empty string when the indexed root isn't in a git repo.
72
- *
73
- * Format: " @ ab12cd3"
74
- */
75
- function shaTag(idx: GraphIndex): string {
76
- const sha = idx.graph.indexedSha;
77
- if (!sha) return '';
78
- return ' @ ' + sha.slice(0, 7);
79
- }
80
-
81
- function fmtSymbol(s: SymbolRecord, idx: GraphIndex, index?: number): string {
82
- const prefix = index !== undefined ? `${index + 1}. ` : '';
83
- const parentTag = s.parent ? `${s.parent}.` : '';
84
- const exp = s.exported ? ' [exported]' : '';
85
- // Evidence anchor: file:line @ sha (when in a git repo) — agent can
86
- // include this verbatim in its reply for the user to verify.
87
- return (
88
- `${prefix}${parentTag}${s.name} (${s.kind}) ${s.file}:${s.line}${shaTag(idx)}${exp}\n` +
89
- ` ${s.signature}`
90
- );
91
- }
92
-
93
- function fmtEdge(e: CallEdge, idx: GraphIndex, index?: number): string {
94
- const prefix = index !== undefined ? `${index + 1}. ` : '';
95
- // We don't know upfront whether this is a callers or callees listing, so the
96
- // safest thing is to show both ends.
97
- const fromSym = idx.findById(e.fromId);
98
- const fromName = fromSym
99
- ? `${fromSym.parent ? fromSym.parent + '.' : ''}${fromSym.name}`
100
- : e.fromId;
101
- const toLabel = e.toId
102
- ? (() => {
103
- const t = idx.findById(e.toId);
104
- return t ? `${t.parent ? t.parent + '.' : ''}${t.name}` : e.toName;
105
- })()
106
- : `${e.toName} <unresolved>`;
107
- // Evidence anchor on the call site (the file:line where the call occurs).
108
- return `${prefix}${fromName} → ${toLabel} (${e.file}:${e.line}${shaTag(idx)})`;
109
- }
110
-
111
- // ----------------------------------------------------------------------------
112
- // Tool catalog (sent to clients via tools/list)
113
- // ----------------------------------------------------------------------------
114
-
115
- const TOOLS = [
116
- {
117
- name: 'gp_stats',
118
- description:
119
- 'Show GraphPilot index health for a repo (symbol count, edge count, ' +
120
- 'when indexed). Use this to confirm the index is fresh before asking ' +
121
- 'structural questions.',
122
- inputSchema: {
123
- type: 'object',
124
- properties: {
125
- path: { type: 'string', description: 'Repo path. Default: cwd.' },
126
- },
127
- additionalProperties: false,
128
- },
129
- },
130
- {
131
- name: 'gp_index',
132
- description:
133
- 'Index or re-index a TypeScript/JavaScript repo into GraphPilot. Call ' +
134
- 'this when the codebase has changed materially or when no index exists.',
135
- inputSchema: {
136
- type: 'object',
137
- properties: {
138
- path: {
139
- type: 'string',
140
- description: 'Repo path to index. Default: cwd.',
141
- },
142
- },
143
- additionalProperties: false,
144
- },
145
- },
146
- {
147
- name: 'gp_recall',
148
- description:
149
- 'Look up symbols (functions, classes, methods, types, interfaces) by ' +
150
- 'name. Returns kind, location, and signature. Default: exact ' +
151
- 'case-insensitive. Pass substring:true for partial matches.',
152
- inputSchema: {
153
- type: 'object',
154
- properties: {
155
- query: {
156
- type: 'string',
157
- description: 'Symbol name to look up.',
158
- },
159
- limit: {
160
- type: 'integer',
161
- minimum: 1,
162
- maximum: 50,
163
- description: 'Max results (default 10).',
164
- },
165
- substring: {
166
- type: 'boolean',
167
- description: 'Enable substring match (default false).',
168
- },
169
- path: {
170
- type: 'string',
171
- description: 'Repo path. Default: cwd.',
172
- },
173
- },
174
- required: ['query'],
175
- additionalProperties: false,
176
- },
177
- },
178
- {
179
- name: 'gp_callers',
180
- description:
181
- 'List callers of a symbol (who calls it) or callees (what it calls). ' +
182
- "Use direction='callers' for impact analysis ('what breaks if I " +
183
- "change this?'); direction='callees' to see what a function depends on.",
184
- inputSchema: {
185
- type: 'object',
186
- properties: {
187
- symbol: {
188
- type: 'string',
189
- description: 'Symbol name or full id.',
190
- },
191
- direction: {
192
- type: 'string',
193
- enum: ['callers', 'callees'],
194
- description: "Default 'callers'.",
195
- },
196
- limit: {
197
- type: 'integer',
198
- minimum: 1,
199
- maximum: 100,
200
- description: 'Max edges (default 50).',
201
- },
202
- includeUnresolved: {
203
- type: 'boolean',
204
- description: 'Include external/stdlib calls (default true).',
205
- },
206
- path: {
207
- type: 'string',
208
- description: 'Repo path. Default: cwd.',
209
- },
210
- },
211
- required: ['symbol'],
212
- additionalProperties: false,
213
- },
214
- },
215
- {
216
- name: 'gp_impact',
217
- description:
218
- 'Analyze the BLAST RADIUS of changing a symbol. Returns direct callers, ' +
219
- 'transitive callers (default depth 3), tests likely affected, and ' +
220
- 'whether the symbol is part of the public API (exported). ' +
221
- 'Use this BEFORE proposing a rename, signature change, or behavior ' +
222
- 'change — it answers "what breaks if I change X?" in one call instead ' +
223
- 'of composing multiple gp_callers queries. ' +
224
- 'Pass `since: <commit|branch>` to restrict callers to files changed ' +
225
- 'since that ref — ideal for PR review ("what does this branch touch?") ' +
226
- 'and refactor scoping.',
227
- inputSchema: {
228
- type: 'object',
229
- properties: {
230
- symbol: {
231
- type: 'string',
232
- description: 'Symbol name or full id to analyze.',
233
- },
234
- depth: {
235
- type: 'integer',
236
- minimum: 1,
237
- maximum: 5,
238
- description: 'BFS depth over the callers graph. Default 3.',
239
- },
240
- path: {
241
- type: 'string',
242
- description: 'Repo path. Default: cwd.',
243
- },
244
- since: {
245
- type: 'string',
246
- description:
247
- 'Optional commit SHA, tag, or branch. When set, restricts ' +
248
- 'callers to files changed between that ref and HEAD.',
249
- },
250
- },
251
- required: ['symbol'],
252
- additionalProperties: false,
253
- },
254
- },
255
- ] as const;
256
-
257
- // ----------------------------------------------------------------------------
258
- // Tool handlers
259
- // ----------------------------------------------------------------------------
260
-
261
- interface ToolResult {
262
- text: string;
263
- results: number;
264
- isError?: boolean;
265
- }
266
-
267
- function handleGpStats(args: GpStatsArgs): ToolResult {
268
- const out = getOrLoadIndex(args.path);
269
- if ('error' in out) {
270
- return { text: out.error, results: 0, isError: true };
271
- }
272
- const { idx } = out;
273
- const s = idx.stats;
274
- const g = idx.graph;
275
- // Git provenance — surface branch + short SHA so the agent can cite
276
- // the exact commit the index was built against. Omitted gracefully
277
- // when the indexed root isn't a git repo.
278
- const gitLines: string[] = [];
279
- if (g.indexedBranch) gitLines.push(`Branch: ${g.indexedBranch}`);
280
- if (g.indexedSha) gitLines.push(`Commit SHA: ${g.indexedSha.slice(0, 7)}`);
281
- const text = [
282
- `Repo: ${g.rootPath}`,
283
- `Repo id: ${g.repoId}`,
284
- `Indexed at: ${g.indexedAt}`,
285
- ...gitLines,
286
- `Files: ${g.filesIndexed}`,
287
- `Symbols: ${s.symbols}`,
288
- `Calls: ${s.edges} (${s.resolvedEdges} resolved)`,
289
- ].join('\n');
290
- return { text, results: 1 };
291
- }
292
-
293
- async function handleGpIndex(args: GpIndexArgs): Promise<ToolResult> {
294
- const requested = resolve(args.path ?? process.cwd());
295
- const { root, redirected } = resolveIndexRoot(requested);
296
- const refusal = validateRootPath(root);
297
- if (refusal) return { text: `Error: ${refusal}`, results: 0, isError: true };
298
-
299
- const result = await indexDirectory(root);
300
- const graph: Graph = {
301
- version: 1,
302
- repoId: repoIdFor(root),
303
- rootPath: root,
304
- indexedAt: new Date().toISOString(),
305
- filesIndexed: result.filesIndexed,
306
- symbolCount: result.symbols.length,
307
- edgeCount: result.edges.length,
308
- symbols: result.symbols,
309
- edges: result.edges,
310
- indexedSha: result.git.sha,
311
- indexedBranch: result.git.branch,
312
- };
313
- saveGraph(graph);
314
- // After re-index, drop the cached GraphIndex for this root so subsequent
315
- // calls see fresh data.
316
- invalidateCache(root);
317
- const resolved = result.edges.filter((e) => e.toId !== null).length;
318
- // Mirror cmdIndex: surface git provenance in the agent-visible output so
319
- // the agent can cite the exact commit it just indexed against.
320
- let gitLine = '';
321
- if (result.git.shortSha || result.git.branch) {
322
- const parts: string[] = [];
323
- if (result.git.branch) parts.push(`branch ${result.git.branch}`);
324
- if (result.git.shortSha) parts.push(`sha ${result.git.shortSha}`);
325
- gitLine = ` Git: ${parts.join(' @ ')}\n`;
326
- }
327
- const wtNote = redirected
328
- ? `(re-rooted to git worktree top; requested path was ${requested})\n`
329
- : '';
330
- const text =
331
- `Indexed ${root}\n` +
332
- wtNote +
333
- ` Files: ${result.filesIndexed}\n` +
334
- ` Symbols: ${result.symbols.length}\n` +
335
- ` Calls: ${result.edges.length} (${resolved} resolved)\n` +
336
- gitLine +
337
- ` Took: ${result.durationMs}ms`;
338
- return { text, results: 1 };
339
- }
340
-
341
- function handleGpRecall(args: GpRecallArgs): ToolResult {
342
- const out = getOrLoadIndex(args.path);
343
- if ('error' in out) {
344
- return { text: out.error, results: 0, isError: true };
345
- }
346
- const { idx } = out;
347
- const matches = idx.findByName(args.query, {
348
- limit: args.limit,
349
- substring: args.substring,
350
- });
351
- if (matches.length === 0) {
352
- return {
353
- text: `No symbols match "${args.query}".`,
354
- results: 0,
355
- };
356
- }
357
- const header = `Found ${matches.length} symbol(s) matching "${args.query}":\n`;
358
- const body = matches.map((s, i) => fmtSymbol(s, idx, i)).join('\n\n');
359
- return { text: header + body, results: matches.length };
360
- }
361
-
362
- function handleGpCallers(args: GpCallersArgs): ToolResult {
363
- const out = getOrLoadIndex(args.path);
364
- if ('error' in out) {
365
- return { text: out.error, results: 0, isError: true };
366
- }
367
- const { idx } = out;
368
- const direction = args.direction ?? 'callers';
369
- const target = idx.resolveSymbol(args.symbol);
370
- if (!target) {
371
- return {
372
- text: `No symbol found matching "${args.symbol}".`,
373
- results: 0,
374
- isError: true,
375
- };
376
- }
377
-
378
- const edges =
379
- direction === 'callers'
380
- ? idx.callers(target.id, { limit: args.limit })
381
- : idx.callees(target.id, {
382
- limit: args.limit,
383
- includeUnresolved: args.includeUnresolved !== false,
384
- });
385
-
386
- if (edges.length === 0) {
387
- const label = direction === 'callers' ? 'callers' : 'callees';
388
- return {
389
- text: `No ${label} found for ${target.name} (${target.file}:${target.line}).`,
390
- results: 0,
391
- };
392
- }
393
-
394
- const verb = direction === 'callers' ? 'callers of' : 'callees of';
395
- // Evidence anchor on the target itself: file:line @ sha so the agent can
396
- // verify the symbol it's about to act on really lives where we say.
397
- const header =
398
- `${edges.length} ${verb} ${target.name} ` + `(${target.file}:${target.line}${shaTag(idx)}):\n`;
399
- const body = edges.map((e, i) => fmtEdge(e, idx, i)).join('\n');
400
- return { text: header + body, results: edges.length };
401
- }
402
-
403
- /**
404
- * Format an ImpactCaller for the agent's text output. Includes via-symbol
405
- * context when depth > 1 so the agent can trace the chain.
406
- */
407
- function fmtImpactCaller(c: ImpactCaller, idx: GraphIndex): string {
408
- // Evidence anchor on every caller — file:line @ sha lets the agent
409
- // (and ultimately the user) verify each impact entry.
410
- const head = ` ${c.symbol.name} (${c.symbol.file}:${c.symbol.line}${shaTag(idx)})`;
411
- if (c.depth === 1) return head;
412
- // For transitive callers, show the immediate hop the edge connects to —
413
- // the symbol that this caller called (one closer to the target).
414
- const via = c.edge.toId ? idx.findById(c.edge.toId) : null;
415
- const viaText = via ? ` ← calls ${via.name}` : ` ← calls ${c.edge.toName}`;
416
- return `${head} [depth ${c.depth}]${viaText}`;
417
- }
418
-
419
- function fmtImpactReport(
420
- report: ImpactResult,
421
- idx: GraphIndex,
422
- diff: { since?: string; changedFileCount: number | null } = { changedFileCount: null },
423
- ): string {
424
- const t = report.target;
425
- const lines: string[] = [];
426
- lines.push(`Impact of changing ${t.name} (${t.file}:${t.line}, kind=${t.kind}):`);
427
- if (diff.since !== undefined) {
428
- lines.push(
429
- `(differential mode: scoped to ${diff.changedFileCount ?? 0} file(s) changed since ${diff.since})`,
430
- );
431
- }
432
- lines.push('');
433
-
434
- lines.push(`Direct callers (${report.stats.directCount}):`);
435
- if (report.directCallers.length === 0) {
436
- lines.push(' (none in indexed code)');
437
- } else {
438
- for (const c of report.directCallers) lines.push(fmtImpactCaller(c, idx));
439
- }
440
- lines.push('');
441
-
442
- if (report.transitiveCallers.length > 0) {
443
- lines.push(`Transitive callers (${report.stats.transitiveCount}):`);
444
- for (const c of report.transitiveCallers) lines.push(fmtImpactCaller(c, idx));
445
- lines.push('');
446
- }
447
-
448
- lines.push(`Tests likely affected (${report.stats.testCount}):`);
449
- if (report.testsAffected.length === 0) {
450
- lines.push(' (no test files reach this symbol)');
451
- } else {
452
- for (const c of report.testsAffected) {
453
- lines.push(` ${c.symbol.file}:${c.symbol.line} — ${c.symbol.name}`);
454
- }
455
- }
456
- lines.push('');
457
-
458
- lines.push(`Public API: ${report.publicApi.exported ? 'YES (exported)' : 'no (internal)'}`);
459
- lines.push(` ${report.publicApi.reason}`);
460
- lines.push('');
461
-
462
- const totalCallers = report.stats.directCount + report.stats.transitiveCount;
463
- const summary =
464
- totalCallers === 0
465
- ? `Summary: ${t.name} has no callers in the indexed code. ` +
466
- `Safe to rename in-repo${
467
- report.publicApi.exported
468
- ? '; external consumers (if any) are not visible to this index.'
469
- : '.'
470
- }`
471
- : `Summary: ${totalCallers} callsite(s) across ` +
472
- `${report.stats.sourceFileCount} file(s)` +
473
- (report.stats.testCount > 0 ? ` + ${report.stats.testCount} test(s)` : '') +
474
- `. ` +
475
- (report.publicApi.exported
476
- ? `Renaming is a BREAKING change for the module's public API.`
477
- : `Renaming is contained within the repo.`);
478
- lines.push(summary);
479
-
480
- if (report.stats.truncated) {
481
- lines.push('');
482
- lines.push(
483
- '(Output truncated — per-level cap hit. Re-run with a smaller depth ' +
484
- 'or query specific callers via gp_callers for full detail.)',
485
- );
486
- }
487
-
488
- return lines.join('\n');
489
- }
490
-
491
- async function handleGpImpact(args: GpImpactArgs): Promise<ToolResult> {
492
- const out = getOrLoadIndex(args.path);
493
- if ('error' in out) {
494
- return { text: out.error, results: 0, isError: true };
495
- }
496
- const { idx, root } = out;
497
-
498
- let changedFiles: Set<string> | null = null;
499
- if (args.since !== undefined) {
500
- changedFiles = await getChangedFiles(root, args.since);
501
- if (changedFiles === null) {
502
- return {
503
- text:
504
- `Could not compute diff against "${args.since}" — either the ref ` +
505
- `does not resolve to a commit, or ${root} is not a git repo. ` +
506
- `Drop the \`since\` argument to see the full blast radius.`,
507
- results: 0,
508
- isError: true,
509
- };
510
- }
511
- }
512
-
513
- const report = analyzeImpact(idx, args.symbol, {
514
- depth: args.depth,
515
- changedFiles,
516
- });
517
- if (!report) {
518
- return {
519
- text: `No symbol found matching "${args.symbol}".`,
520
- results: 0,
521
- isError: true,
522
- };
523
- }
524
-
525
- const text = fmtImpactReport(report, idx, {
526
- since: args.since,
527
- changedFileCount: changedFiles?.size ?? null,
528
- });
529
- const totalResults = report.stats.directCount + report.stats.transitiveCount;
530
- return { text, results: totalResults };
531
- }
532
-
533
- // ----------------------------------------------------------------------------
534
- // Server builder + dispatcher
535
- // ----------------------------------------------------------------------------
536
-
537
- export function buildMcpServer(): Server {
538
- const server = new Server(
539
- { name: SERVER_NAME, version: SERVER_VERSION },
540
- { capabilities: { tools: {} } },
541
- );
542
-
543
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
544
- tools: TOOLS as unknown as Array<{
545
- name: string;
546
- description: string;
547
- inputSchema: object;
548
- }>,
549
- }));
550
-
551
- server.setRequestHandler(CallToolRequestSchema, async (req) => {
552
- const { name, arguments: args } = req.params;
553
- const rawArgs = (args ?? {}) as Record<string, unknown>;
554
-
555
- // We need to know the repo path *before* validation to drive the log,
556
- // so the log captures even invalid-input attempts. Fall back to cwd.
557
- // Resolve to the worktree top so per-tool calls from a subdir land in
558
- // the same interaction log as the rest of the branch's work.
559
- const requestedLogPath = typeof rawArgs.path === 'string' ? rawArgs.path : process.cwd();
560
- const repoRootForLog = resolveIndexRoot(requestedLogPath).root;
561
-
562
- return withInteractionLog(repoRootForLog, name, rawArgs, async () => {
563
- // Validate first
564
- let result: ToolResult;
565
- switch (name) {
566
- case 'gp_stats': {
567
- const v = validateGpStats(rawArgs);
568
- if (!v.ok) {
569
- result = { text: `Invalid input: ${v.error}`, results: 0, isError: true };
570
- break;
571
- }
572
- result = handleGpStats(v.value);
573
- break;
574
- }
575
- case 'gp_index': {
576
- const v = validateGpIndex(rawArgs);
577
- if (!v.ok) {
578
- result = { text: `Invalid input: ${v.error}`, results: 0, isError: true };
579
- break;
580
- }
581
- result = await handleGpIndex(v.value);
582
- break;
583
- }
584
- case 'gp_recall': {
585
- const v = validateGpRecall(rawArgs);
586
- if (!v.ok) {
587
- result = { text: `Invalid input: ${v.error}`, results: 0, isError: true };
588
- break;
589
- }
590
- result = handleGpRecall(v.value);
591
- break;
592
- }
593
- case 'gp_callers': {
594
- const v = validateGpCallers(rawArgs);
595
- if (!v.ok) {
596
- result = { text: `Invalid input: ${v.error}`, results: 0, isError: true };
597
- break;
598
- }
599
- result = handleGpCallers(v.value);
600
- break;
601
- }
602
- case 'gp_impact': {
603
- const v = validateGpImpact(rawArgs);
604
- if (!v.ok) {
605
- result = { text: `Invalid input: ${v.error}`, results: 0, isError: true };
606
- break;
607
- }
608
- result = await handleGpImpact(v.value);
609
- break;
610
- }
611
- default:
612
- result = { text: `Unknown tool: ${name}`, results: 0, isError: true };
613
- }
614
-
615
- return {
616
- // Caller (this lambda) returns to the dispatcher which sends to the
617
- // MCP client. We also return interaction-log metadata.
618
- value: {
619
- content: [{ type: 'text', text: result.text }] as const,
620
- isError: result.isError,
621
- },
622
- results: result.results,
623
- error: result.isError ? result.text.slice(0, 200) : undefined,
624
- };
625
- }).then((v) => v);
626
- });
627
-
628
- return server;
629
- }
630
-
631
- export async function startMcpServer(): Promise<void> {
632
- const server = buildMcpServer();
633
- const transport = new StdioServerTransport();
634
- await server.connect(transport);
635
- process.stderr.write(`[graphpilot] MCP server ready (stdio).\n`);
636
-
637
- // server.connect() resolves once handlers are wired — it does NOT block.
638
- // We have to keep this promise pending until the client disconnects, or the
639
- // CLI's `process.exit(0)` will kill us before the initialize handshake
640
- // completes. Resolve on either the transport's onclose, or stdin EOF.
641
- await new Promise<void>((resolve) => {
642
- let done = false;
643
- const finish = () => {
644
- if (done) return;
645
- done = true;
646
- resolve();
647
- };
648
- transport.onclose = finish;
649
- process.stdin.once('end', finish);
650
- process.stdin.once('close', finish);
651
- });
652
- }