typegraph-mcp 0.9.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.
- package/.claude-plugin/plugin.json +17 -0
- package/.cursor-plugin/plugin.json +17 -0
- package/.mcp.json +10 -0
- package/LICENSE +21 -0
- package/README.md +451 -0
- package/benchmark.ts +735 -0
- package/check.ts +459 -0
- package/cli.ts +778 -0
- package/commands/check.md +23 -0
- package/commands/test.md +23 -0
- package/config.ts +50 -0
- package/gemini-extension.json +16 -0
- package/graph-queries.ts +462 -0
- package/hooks/hooks.json +15 -0
- package/module-graph.ts +507 -0
- package/package.json +39 -0
- package/scripts/ensure-deps.sh +34 -0
- package/server.ts +837 -0
- package/skills/code-exploration/SKILL.md +55 -0
- package/skills/dependency-audit/SKILL.md +50 -0
- package/skills/impact-analysis/SKILL.md +52 -0
- package/skills/refactor-safety/SKILL.md +50 -0
- package/skills/tool-selection/SKILL.md +79 -0
- package/smoke-test.ts +500 -0
- package/tsserver-client.ts +413 -0
package/server.ts
ADDED
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* TypeGraph MCP Server — Type-aware codebase navigation for AI coding agents.
|
|
4
|
+
*
|
|
5
|
+
* Bridges MCP protocol (stdin/stdout) to tsserver (child process pipes).
|
|
6
|
+
* Provides 14 tools for definition, references, type info, symbol search,
|
|
7
|
+
* call chain tracing, blast radius analysis, module export inspection,
|
|
8
|
+
* and module graph queries (dependency trees, cycles, paths, boundaries).
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* npx tsx server.ts
|
|
12
|
+
*
|
|
13
|
+
* Environment:
|
|
14
|
+
* TYPEGRAPH_PROJECT_ROOT — project root (default: cwd)
|
|
15
|
+
* TYPEGRAPH_TSCONFIG — tsconfig path (default: ./tsconfig.json)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
19
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
import { TsServerClient, type NavBarItem } from "./tsserver-client.js";
|
|
22
|
+
import { buildGraph, startWatcher, type ModuleGraph } from "./module-graph.js";
|
|
23
|
+
import {
|
|
24
|
+
dependencyTree,
|
|
25
|
+
dependents,
|
|
26
|
+
importCycles,
|
|
27
|
+
shortestPath,
|
|
28
|
+
subgraph,
|
|
29
|
+
moduleBoundary,
|
|
30
|
+
} from "./graph-queries.js";
|
|
31
|
+
import * as fs from "node:fs";
|
|
32
|
+
import * as path from "node:path";
|
|
33
|
+
import { resolveConfig } from "./config.js";
|
|
34
|
+
|
|
35
|
+
// ─── Configuration ───────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
const { projectRoot, tsconfigPath } = resolveConfig(import.meta.dirname);
|
|
38
|
+
|
|
39
|
+
const log = (...args: unknown[]) => console.error("[typegraph]", ...args);
|
|
40
|
+
|
|
41
|
+
// ─── Initialize ──────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
const client = new TsServerClient(projectRoot, tsconfigPath);
|
|
44
|
+
|
|
45
|
+
// Module graph — initialized in main(), used by graph tools
|
|
46
|
+
let moduleGraph: ModuleGraph;
|
|
47
|
+
|
|
48
|
+
const mcpServer = new McpServer({
|
|
49
|
+
name: "typegraph",
|
|
50
|
+
version: "1.0.0",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/** Read a preview line from a file at a 1-based line number */
|
|
56
|
+
function readPreview(file: string, line: number): string {
|
|
57
|
+
try {
|
|
58
|
+
const absPath = client.resolvePath(file);
|
|
59
|
+
const content = fs.readFileSync(absPath, "utf-8");
|
|
60
|
+
return content.split("\n")[line - 1]?.trim() ?? "";
|
|
61
|
+
} catch {
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Search a navbar tree recursively for a symbol by name */
|
|
67
|
+
function findInNavBar(
|
|
68
|
+
items: NavBarItem[],
|
|
69
|
+
symbol: string
|
|
70
|
+
): { line: number; offset: number; kind: string } | null {
|
|
71
|
+
for (const item of items) {
|
|
72
|
+
if (item.text === symbol && item.spans.length > 0) {
|
|
73
|
+
const span = item.spans[0]!;
|
|
74
|
+
return { line: span.start.line, offset: span.start.offset, kind: item.kind };
|
|
75
|
+
}
|
|
76
|
+
if (item.childItems?.length > 0) {
|
|
77
|
+
const found = findInNavBar(item.childItems, symbol);
|
|
78
|
+
if (found) return found;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Resolve symbol to coordinates: try navbar first, fall back to navto */
|
|
85
|
+
async function resolveSymbol(
|
|
86
|
+
file: string,
|
|
87
|
+
symbol: string
|
|
88
|
+
): Promise<{
|
|
89
|
+
file: string;
|
|
90
|
+
line: number;
|
|
91
|
+
column: number;
|
|
92
|
+
kind: string;
|
|
93
|
+
preview: string;
|
|
94
|
+
} | null> {
|
|
95
|
+
// Strategy 1: navbar (file-scoped AST search)
|
|
96
|
+
const bar = await client.navbar(file);
|
|
97
|
+
const found = findInNavBar(bar, symbol);
|
|
98
|
+
if (found) {
|
|
99
|
+
return {
|
|
100
|
+
file,
|
|
101
|
+
line: found.line,
|
|
102
|
+
column: found.offset,
|
|
103
|
+
kind: found.kind,
|
|
104
|
+
preview: readPreview(file, found.line),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Strategy 2: navto (project-wide search, filtered by file)
|
|
109
|
+
const items = await client.navto(symbol, 10, file);
|
|
110
|
+
// Prefer exact match in the specified file
|
|
111
|
+
const inFile = items.find((i) => i.name === symbol && i.file === file);
|
|
112
|
+
const best = inFile ?? items.find((i) => i.name === symbol) ?? items[0];
|
|
113
|
+
|
|
114
|
+
if (best) {
|
|
115
|
+
return {
|
|
116
|
+
file: best.file,
|
|
117
|
+
line: best.start.line,
|
|
118
|
+
column: best.start.offset,
|
|
119
|
+
kind: best.kind,
|
|
120
|
+
preview: readPreview(best.file, best.start.line),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─── Tool Schemas ────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Shared schema for tools that accept either coordinates (file+line+column)
|
|
131
|
+
* or a symbol name (file+symbol). The MCP SDK requires a flat object schema.
|
|
132
|
+
*/
|
|
133
|
+
const locationOrSymbol = {
|
|
134
|
+
file: z.string().describe("File path (relative to project root or absolute)"),
|
|
135
|
+
line: z
|
|
136
|
+
.number()
|
|
137
|
+
.int()
|
|
138
|
+
.positive()
|
|
139
|
+
.optional()
|
|
140
|
+
.describe("Line number (1-based). Required if symbol is not provided."),
|
|
141
|
+
column: z
|
|
142
|
+
.number()
|
|
143
|
+
.int()
|
|
144
|
+
.positive()
|
|
145
|
+
.optional()
|
|
146
|
+
.describe("Column/offset (1-based). Required if symbol is not provided."),
|
|
147
|
+
symbol: z.string().optional().describe("Symbol name to find. Alternative to line+column."),
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/** Resolve params to coordinates: use line+column if provided, else find symbol */
|
|
151
|
+
async function resolveParams(params: {
|
|
152
|
+
file: string;
|
|
153
|
+
line?: number;
|
|
154
|
+
column?: number;
|
|
155
|
+
symbol?: string;
|
|
156
|
+
}): Promise<{ file: string; line: number; column: number } | { error: string }> {
|
|
157
|
+
if (params.line !== undefined && params.column !== undefined) {
|
|
158
|
+
return { file: params.file, line: params.line, column: params.column };
|
|
159
|
+
}
|
|
160
|
+
if (params.symbol) {
|
|
161
|
+
const resolved = await resolveSymbol(params.file, params.symbol);
|
|
162
|
+
if (!resolved) {
|
|
163
|
+
return { error: `Symbol "${params.symbol}" not found in ${params.file}` };
|
|
164
|
+
}
|
|
165
|
+
return { file: resolved.file, line: resolved.line, column: resolved.column };
|
|
166
|
+
}
|
|
167
|
+
return { error: "Either line+column or symbol must be provided" };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─── Tool 1: ts_find_symbol ─────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
mcpServer.tool(
|
|
173
|
+
"ts_find_symbol",
|
|
174
|
+
"Find a symbol's location in a file by name. Entry point for navigating without exact coordinates.",
|
|
175
|
+
{
|
|
176
|
+
file: z.string().describe("File to search in (relative or absolute path)"),
|
|
177
|
+
symbol: z.string().describe("Symbol name to find"),
|
|
178
|
+
},
|
|
179
|
+
async ({ file, symbol }) => {
|
|
180
|
+
const result = await resolveSymbol(file, symbol);
|
|
181
|
+
if (!result) {
|
|
182
|
+
return {
|
|
183
|
+
content: [
|
|
184
|
+
{
|
|
185
|
+
type: "text" as const,
|
|
186
|
+
text: JSON.stringify({ error: `Symbol "${symbol}" not found in ${file}` }),
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
content: [{ type: "text" as const, text: JSON.stringify(result) }],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// ─── Tool 2: ts_definition ──────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
mcpServer.tool(
|
|
200
|
+
"ts_definition",
|
|
201
|
+
"Go to definition. Resolves through imports, re-exports, barrel files, interfaces, generics. Provide either line+column coordinates or a symbol name.",
|
|
202
|
+
locationOrSymbol,
|
|
203
|
+
async (params) => {
|
|
204
|
+
const loc = await resolveParams(params);
|
|
205
|
+
if ("error" in loc) {
|
|
206
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(loc) }] };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const defs = await client.definition(loc.file, loc.line, loc.column);
|
|
210
|
+
if (defs.length === 0) {
|
|
211
|
+
return {
|
|
212
|
+
content: [
|
|
213
|
+
{
|
|
214
|
+
type: "text" as const,
|
|
215
|
+
text: JSON.stringify({ definitions: [], source: readPreview(loc.file, loc.line) }),
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const results = defs.map((d) => ({
|
|
222
|
+
file: d.file,
|
|
223
|
+
line: d.start.line,
|
|
224
|
+
column: d.start.offset,
|
|
225
|
+
preview: readPreview(d.file, d.start.line),
|
|
226
|
+
}));
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
content: [{ type: "text" as const, text: JSON.stringify({ definitions: results }) }],
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// ─── Tool 3: ts_references ──────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
mcpServer.tool(
|
|
237
|
+
"ts_references",
|
|
238
|
+
"Find all references to a symbol. Returns semantic code references only (not string matches). Provide either line+column or symbol name.",
|
|
239
|
+
locationOrSymbol,
|
|
240
|
+
async (params) => {
|
|
241
|
+
const loc = await resolveParams(params);
|
|
242
|
+
if ("error" in loc) {
|
|
243
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(loc) }] };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const refs = await client.references(loc.file, loc.line, loc.column);
|
|
247
|
+
const results = refs.map((r) => ({
|
|
248
|
+
file: r.file,
|
|
249
|
+
line: r.start.line,
|
|
250
|
+
column: r.start.offset,
|
|
251
|
+
preview: r.lineText.trim(),
|
|
252
|
+
isDefinition: r.isDefinition,
|
|
253
|
+
}));
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
content: [
|
|
257
|
+
{
|
|
258
|
+
type: "text" as const,
|
|
259
|
+
text: JSON.stringify({ references: results, count: results.length }),
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
// ─── Tool 4: ts_type_info ───────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
mcpServer.tool(
|
|
269
|
+
"ts_type_info",
|
|
270
|
+
"Get the TypeScript type and documentation for a symbol. Returns the same info you see when hovering in VS Code. Provide either line+column or symbol name.",
|
|
271
|
+
locationOrSymbol,
|
|
272
|
+
async (params) => {
|
|
273
|
+
const loc = await resolveParams(params);
|
|
274
|
+
if ("error" in loc) {
|
|
275
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(loc) }] };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const info = await client.quickinfo(loc.file, loc.line, loc.column);
|
|
279
|
+
if (!info) {
|
|
280
|
+
return {
|
|
281
|
+
content: [
|
|
282
|
+
{
|
|
283
|
+
type: "text" as const,
|
|
284
|
+
text: JSON.stringify({
|
|
285
|
+
type: null,
|
|
286
|
+
documentation: null,
|
|
287
|
+
source: readPreview(loc.file, loc.line),
|
|
288
|
+
}),
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
content: [
|
|
296
|
+
{
|
|
297
|
+
type: "text" as const,
|
|
298
|
+
text: JSON.stringify({
|
|
299
|
+
type: info.displayString,
|
|
300
|
+
documentation: info.documentation || null,
|
|
301
|
+
kind: info.kind,
|
|
302
|
+
}),
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
// ─── Tool 5: ts_navigate_to ─────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
mcpServer.tool(
|
|
312
|
+
"ts_navigate_to",
|
|
313
|
+
"Search for a symbol across the entire project without knowing which file it's in. Returns matching declarations. Optionally provide a file hint to also search that file's navbar (useful for object literal keys like RPC handlers that navto doesn't index).",
|
|
314
|
+
{
|
|
315
|
+
symbol: z.string().describe("Symbol name to search for"),
|
|
316
|
+
file: z
|
|
317
|
+
.string()
|
|
318
|
+
.optional()
|
|
319
|
+
.describe(
|
|
320
|
+
"Optional file to also search via navbar (covers object literal keys not indexed by navto)"
|
|
321
|
+
),
|
|
322
|
+
maxResults: z
|
|
323
|
+
.number()
|
|
324
|
+
.int()
|
|
325
|
+
.positive()
|
|
326
|
+
.optional()
|
|
327
|
+
.default(10)
|
|
328
|
+
.describe("Maximum results (default 10)"),
|
|
329
|
+
},
|
|
330
|
+
async ({ symbol, file, maxResults }) => {
|
|
331
|
+
const items = await client.navto(symbol, maxResults);
|
|
332
|
+
const results = items.map((item) => ({
|
|
333
|
+
file: item.file,
|
|
334
|
+
line: item.start.line,
|
|
335
|
+
column: item.start.offset,
|
|
336
|
+
kind: item.kind,
|
|
337
|
+
containerName: item.containerName,
|
|
338
|
+
matchKind: item.matchKind,
|
|
339
|
+
}));
|
|
340
|
+
|
|
341
|
+
// Supplement with navbar search when a file hint is provided.
|
|
342
|
+
// This covers object literal property keys (e.g. RPC handlers)
|
|
343
|
+
// that tsserver's navto command doesn't index.
|
|
344
|
+
if (file) {
|
|
345
|
+
const navbarHit = await resolveSymbol(file, symbol);
|
|
346
|
+
if (navbarHit) {
|
|
347
|
+
const alreadyFound = results.some(
|
|
348
|
+
(r) => r.file === navbarHit.file && r.line === navbarHit.line
|
|
349
|
+
);
|
|
350
|
+
if (!alreadyFound) {
|
|
351
|
+
results.unshift({
|
|
352
|
+
file: navbarHit.file,
|
|
353
|
+
line: navbarHit.line,
|
|
354
|
+
column: navbarHit.column,
|
|
355
|
+
kind: navbarHit.kind,
|
|
356
|
+
containerName: "",
|
|
357
|
+
matchKind: "navbar",
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
content: [
|
|
365
|
+
{
|
|
366
|
+
type: "text" as const,
|
|
367
|
+
text: JSON.stringify({ results, count: results.length }),
|
|
368
|
+
},
|
|
369
|
+
],
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
// ─── Tool 6: ts_trace_chain ─────────────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
mcpServer.tool(
|
|
377
|
+
"ts_trace_chain",
|
|
378
|
+
"Automatically follow go-to-definition hops from a symbol, building a call chain from entry point to implementation. Stops when it reaches the bottom or a cycle.",
|
|
379
|
+
{
|
|
380
|
+
file: z.string().describe("Starting file"),
|
|
381
|
+
symbol: z.string().describe("Starting symbol name"),
|
|
382
|
+
maxHops: z
|
|
383
|
+
.number()
|
|
384
|
+
.int()
|
|
385
|
+
.positive()
|
|
386
|
+
.optional()
|
|
387
|
+
.default(5)
|
|
388
|
+
.describe("Maximum hops to follow (default 5)"),
|
|
389
|
+
},
|
|
390
|
+
async ({ file, symbol, maxHops }) => {
|
|
391
|
+
const start = await resolveSymbol(file, symbol);
|
|
392
|
+
if (!start) {
|
|
393
|
+
return {
|
|
394
|
+
content: [
|
|
395
|
+
{
|
|
396
|
+
type: "text" as const,
|
|
397
|
+
text: JSON.stringify({ error: `Symbol "${symbol}" not found in ${file}` }),
|
|
398
|
+
},
|
|
399
|
+
],
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const chain: Array<{
|
|
404
|
+
file: string;
|
|
405
|
+
line: number;
|
|
406
|
+
column: number;
|
|
407
|
+
preview: string;
|
|
408
|
+
}> = [
|
|
409
|
+
{
|
|
410
|
+
file: start.file,
|
|
411
|
+
line: start.line,
|
|
412
|
+
column: start.column,
|
|
413
|
+
preview: start.preview,
|
|
414
|
+
},
|
|
415
|
+
];
|
|
416
|
+
|
|
417
|
+
let current = { file: start.file, line: start.line, offset: start.column };
|
|
418
|
+
|
|
419
|
+
for (let i = 0; i < maxHops; i++) {
|
|
420
|
+
const defs = await client.definition(current.file, current.line, current.offset);
|
|
421
|
+
if (defs.length === 0) break;
|
|
422
|
+
|
|
423
|
+
const def = defs[0]!;
|
|
424
|
+
// Stop if we've reached the same location (self-reference)
|
|
425
|
+
if (def.file === current.file && def.start.line === current.line) break;
|
|
426
|
+
// Stop if we've entered node_modules (external dependency)
|
|
427
|
+
if (def.file.includes("node_modules")) break;
|
|
428
|
+
|
|
429
|
+
const preview = readPreview(def.file, def.start.line);
|
|
430
|
+
chain.push({
|
|
431
|
+
file: def.file,
|
|
432
|
+
line: def.start.line,
|
|
433
|
+
column: def.start.offset,
|
|
434
|
+
preview,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
current = { file: def.file, line: def.start.line, offset: def.start.offset };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
content: [
|
|
442
|
+
{
|
|
443
|
+
type: "text" as const,
|
|
444
|
+
text: JSON.stringify({ chain, hops: chain.length - 1 }),
|
|
445
|
+
},
|
|
446
|
+
],
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
// ─── Tool 7: ts_blast_radius ────────────────────────────────────────────────
|
|
452
|
+
|
|
453
|
+
mcpServer.tool(
|
|
454
|
+
"ts_blast_radius",
|
|
455
|
+
"Analyze the impact of changing a symbol. Finds all references, filters to usage sites, and reports affected files.",
|
|
456
|
+
{
|
|
457
|
+
file: z.string().describe("File containing the symbol"),
|
|
458
|
+
symbol: z.string().describe("Symbol to analyze"),
|
|
459
|
+
},
|
|
460
|
+
async ({ file, symbol }) => {
|
|
461
|
+
const start = await resolveSymbol(file, symbol);
|
|
462
|
+
if (!start) {
|
|
463
|
+
return {
|
|
464
|
+
content: [
|
|
465
|
+
{
|
|
466
|
+
type: "text" as const,
|
|
467
|
+
text: JSON.stringify({ error: `Symbol "${symbol}" not found in ${file}` }),
|
|
468
|
+
},
|
|
469
|
+
],
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const refs = await client.references(start.file, start.line, start.column);
|
|
474
|
+
const callers = refs.filter((r) => !r.isDefinition);
|
|
475
|
+
const filesAffected = [...new Set(callers.map((r) => r.file))];
|
|
476
|
+
|
|
477
|
+
const callerList = callers.map((r) => ({
|
|
478
|
+
file: r.file,
|
|
479
|
+
line: r.start.line,
|
|
480
|
+
preview: r.lineText.trim(),
|
|
481
|
+
}));
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
content: [
|
|
485
|
+
{
|
|
486
|
+
type: "text" as const,
|
|
487
|
+
text: JSON.stringify({
|
|
488
|
+
directCallers: callers.length,
|
|
489
|
+
filesAffected,
|
|
490
|
+
callers: callerList,
|
|
491
|
+
}),
|
|
492
|
+
},
|
|
493
|
+
],
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
// ─── Tool 8: ts_module_exports ──────────────────────────────────────────────
|
|
499
|
+
|
|
500
|
+
mcpServer.tool(
|
|
501
|
+
"ts_module_exports",
|
|
502
|
+
"List all exported symbols from a module with their resolved types. Gives an at-a-glance understanding of what a file provides.",
|
|
503
|
+
{
|
|
504
|
+
file: z.string().describe("File to inspect"),
|
|
505
|
+
},
|
|
506
|
+
async ({ file }) => {
|
|
507
|
+
const bar = await client.navbar(file);
|
|
508
|
+
if (bar.length === 0) {
|
|
509
|
+
return {
|
|
510
|
+
content: [
|
|
511
|
+
{
|
|
512
|
+
type: "text" as const,
|
|
513
|
+
text: JSON.stringify({ error: `No symbols found in ${file}` }),
|
|
514
|
+
},
|
|
515
|
+
],
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// The top-level navbar item is the module itself — its children are exports
|
|
520
|
+
const moduleItem = bar.find((item) => item.kind === "module");
|
|
521
|
+
const topItems = moduleItem?.childItems ?? bar;
|
|
522
|
+
|
|
523
|
+
// Filter to meaningful declarations (skip imports, local vars, etc.)
|
|
524
|
+
const exportKinds = new Set([
|
|
525
|
+
"function",
|
|
526
|
+
"const",
|
|
527
|
+
"class",
|
|
528
|
+
"interface",
|
|
529
|
+
"type",
|
|
530
|
+
"enum",
|
|
531
|
+
"var",
|
|
532
|
+
"let",
|
|
533
|
+
"method",
|
|
534
|
+
]);
|
|
535
|
+
const candidates = topItems.filter((item) => exportKinds.has(item.kind));
|
|
536
|
+
|
|
537
|
+
const exports: Array<{
|
|
538
|
+
symbol: string;
|
|
539
|
+
kind: string;
|
|
540
|
+
line: number;
|
|
541
|
+
type: string | null;
|
|
542
|
+
}> = [];
|
|
543
|
+
|
|
544
|
+
for (const item of candidates) {
|
|
545
|
+
if (!item.spans[0]) continue;
|
|
546
|
+
const span = item.spans[0];
|
|
547
|
+
|
|
548
|
+
// Get type info for this symbol
|
|
549
|
+
const info = await client.quickinfo(file, span.start.line, span.start.offset);
|
|
550
|
+
|
|
551
|
+
exports.push({
|
|
552
|
+
symbol: item.text,
|
|
553
|
+
kind: item.kind,
|
|
554
|
+
line: span.start.line,
|
|
555
|
+
type: info?.displayString ?? null,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return {
|
|
560
|
+
content: [
|
|
561
|
+
{
|
|
562
|
+
type: "text" as const,
|
|
563
|
+
text: JSON.stringify({ file, exports, count: exports.length }),
|
|
564
|
+
},
|
|
565
|
+
],
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
// ─── Graph Tool Helpers ─────────────────────────────────────────────────────
|
|
571
|
+
|
|
572
|
+
/** Convert an absolute path to project-relative */
|
|
573
|
+
function relPath(absPath: string): string {
|
|
574
|
+
return path.relative(projectRoot, absPath);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** Convert a relative or absolute path to absolute */
|
|
578
|
+
function absPath(file: string): string {
|
|
579
|
+
return path.isAbsolute(file) ? file : path.resolve(projectRoot, file);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ─── Tool 9: ts_dependency_tree ─────────────────────────────────────────────
|
|
583
|
+
|
|
584
|
+
mcpServer.tool(
|
|
585
|
+
"ts_dependency_tree",
|
|
586
|
+
"Get the transitive dependency tree (imports) of a file. Shows what a file depends on, directly and transitively.",
|
|
587
|
+
{
|
|
588
|
+
file: z.string().describe("File to analyze (relative or absolute path)"),
|
|
589
|
+
depth: z
|
|
590
|
+
.number()
|
|
591
|
+
.int()
|
|
592
|
+
.positive()
|
|
593
|
+
.optional()
|
|
594
|
+
.describe("Max traversal depth (default: unlimited)"),
|
|
595
|
+
includeTypeOnly: z
|
|
596
|
+
.boolean()
|
|
597
|
+
.optional()
|
|
598
|
+
.default(false)
|
|
599
|
+
.describe("Include type-only imports (default: false)"),
|
|
600
|
+
},
|
|
601
|
+
async ({ file, depth, includeTypeOnly }) => {
|
|
602
|
+
const result = dependencyTree(moduleGraph, absPath(file), { depth, includeTypeOnly });
|
|
603
|
+
return {
|
|
604
|
+
content: [
|
|
605
|
+
{
|
|
606
|
+
type: "text" as const,
|
|
607
|
+
text: JSON.stringify({
|
|
608
|
+
root: relPath(result.root),
|
|
609
|
+
nodes: result.nodes,
|
|
610
|
+
files: result.files.map(relPath),
|
|
611
|
+
}),
|
|
612
|
+
},
|
|
613
|
+
],
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
// ─── Tool 10: ts_dependents ─────────────────────────────────────────────────
|
|
619
|
+
|
|
620
|
+
mcpServer.tool(
|
|
621
|
+
"ts_dependents",
|
|
622
|
+
"Find all files that depend on (import) a given file, directly and transitively. Groups results by package.",
|
|
623
|
+
{
|
|
624
|
+
file: z.string().describe("File to analyze (relative or absolute path)"),
|
|
625
|
+
depth: z
|
|
626
|
+
.number()
|
|
627
|
+
.int()
|
|
628
|
+
.positive()
|
|
629
|
+
.optional()
|
|
630
|
+
.describe("Max traversal depth (default: unlimited)"),
|
|
631
|
+
includeTypeOnly: z
|
|
632
|
+
.boolean()
|
|
633
|
+
.optional()
|
|
634
|
+
.default(false)
|
|
635
|
+
.describe("Include type-only imports (default: false)"),
|
|
636
|
+
},
|
|
637
|
+
async ({ file, depth, includeTypeOnly }) => {
|
|
638
|
+
const result = dependents(moduleGraph, absPath(file), { depth, includeTypeOnly });
|
|
639
|
+
const byPackageRel: Record<string, string[]> = {};
|
|
640
|
+
for (const [pkg, files] of Object.entries(result.byPackage)) {
|
|
641
|
+
byPackageRel[pkg] = files.map(relPath);
|
|
642
|
+
}
|
|
643
|
+
return {
|
|
644
|
+
content: [
|
|
645
|
+
{
|
|
646
|
+
type: "text" as const,
|
|
647
|
+
text: JSON.stringify({
|
|
648
|
+
root: relPath(result.root),
|
|
649
|
+
nodes: result.nodes,
|
|
650
|
+
directCount: result.directCount,
|
|
651
|
+
files: result.files.map(relPath),
|
|
652
|
+
byPackage: byPackageRel,
|
|
653
|
+
}),
|
|
654
|
+
},
|
|
655
|
+
],
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
// ─── Tool 11: ts_import_cycles ──────────────────────────────────────────────
|
|
661
|
+
|
|
662
|
+
mcpServer.tool(
|
|
663
|
+
"ts_import_cycles",
|
|
664
|
+
"Detect circular import dependencies in the project. Returns strongly connected components (cycles) in the import graph.",
|
|
665
|
+
{
|
|
666
|
+
file: z.string().optional().describe("Filter to cycles containing this file"),
|
|
667
|
+
package: z.string().optional().describe("Filter to cycles within this directory"),
|
|
668
|
+
},
|
|
669
|
+
async ({ file, package: pkg }) => {
|
|
670
|
+
const result = importCycles(moduleGraph, {
|
|
671
|
+
file: file ? absPath(file) : undefined,
|
|
672
|
+
package: pkg ? absPath(pkg) : undefined,
|
|
673
|
+
});
|
|
674
|
+
return {
|
|
675
|
+
content: [
|
|
676
|
+
{
|
|
677
|
+
type: "text" as const,
|
|
678
|
+
text: JSON.stringify({
|
|
679
|
+
count: result.count,
|
|
680
|
+
cycles: result.cycles.map((cycle) => cycle.map(relPath)),
|
|
681
|
+
}),
|
|
682
|
+
},
|
|
683
|
+
],
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
// ─── Tool 12: ts_shortest_path ──────────────────────────────────────────────
|
|
689
|
+
|
|
690
|
+
mcpServer.tool(
|
|
691
|
+
"ts_shortest_path",
|
|
692
|
+
"Find the shortest import path between two files. Shows how one module reaches another through the import graph.",
|
|
693
|
+
{
|
|
694
|
+
from: z.string().describe("Source file (relative or absolute path)"),
|
|
695
|
+
to: z.string().describe("Target file (relative or absolute path)"),
|
|
696
|
+
includeTypeOnly: z
|
|
697
|
+
.boolean()
|
|
698
|
+
.optional()
|
|
699
|
+
.default(false)
|
|
700
|
+
.describe("Include type-only imports (default: false)"),
|
|
701
|
+
},
|
|
702
|
+
async ({ from, to, includeTypeOnly }) => {
|
|
703
|
+
const result = shortestPath(moduleGraph, absPath(from), absPath(to), { includeTypeOnly });
|
|
704
|
+
return {
|
|
705
|
+
content: [
|
|
706
|
+
{
|
|
707
|
+
type: "text" as const,
|
|
708
|
+
text: JSON.stringify({
|
|
709
|
+
path: result.path?.map(relPath) ?? null,
|
|
710
|
+
hops: result.hops,
|
|
711
|
+
chain: result.chain.map((c) => ({
|
|
712
|
+
file: relPath(c.file),
|
|
713
|
+
imports: c.imports,
|
|
714
|
+
})),
|
|
715
|
+
}),
|
|
716
|
+
},
|
|
717
|
+
],
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
// ─── Tool 13: ts_subgraph ───────────────────────────────────────────────────
|
|
723
|
+
|
|
724
|
+
mcpServer.tool(
|
|
725
|
+
"ts_subgraph",
|
|
726
|
+
"Extract a subgraph around seed files. Expands by depth hops in the specified direction (imports, dependents, or both).",
|
|
727
|
+
{
|
|
728
|
+
files: z.array(z.string()).describe("Seed files to expand from (relative or absolute paths)"),
|
|
729
|
+
depth: z
|
|
730
|
+
.number()
|
|
731
|
+
.int()
|
|
732
|
+
.positive()
|
|
733
|
+
.optional()
|
|
734
|
+
.default(1)
|
|
735
|
+
.describe("Hops to expand (default: 1)"),
|
|
736
|
+
direction: z
|
|
737
|
+
.enum(["imports", "dependents", "both"])
|
|
738
|
+
.optional()
|
|
739
|
+
.default("both")
|
|
740
|
+
.describe("Direction to expand (default: both)"),
|
|
741
|
+
},
|
|
742
|
+
async ({ files, depth, direction }) => {
|
|
743
|
+
const result = subgraph(moduleGraph, files.map(absPath), { depth, direction });
|
|
744
|
+
return {
|
|
745
|
+
content: [
|
|
746
|
+
{
|
|
747
|
+
type: "text" as const,
|
|
748
|
+
text: JSON.stringify({
|
|
749
|
+
nodes: result.nodes.map(relPath),
|
|
750
|
+
edges: result.edges.map((e) => ({
|
|
751
|
+
from: relPath(e.from),
|
|
752
|
+
to: relPath(e.to),
|
|
753
|
+
specifiers: e.specifiers,
|
|
754
|
+
isTypeOnly: e.isTypeOnly,
|
|
755
|
+
})),
|
|
756
|
+
stats: result.stats,
|
|
757
|
+
}),
|
|
758
|
+
},
|
|
759
|
+
],
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
);
|
|
763
|
+
|
|
764
|
+
// ─── Tool 14: ts_module_boundary ────────────────────────────────────────────
|
|
765
|
+
|
|
766
|
+
mcpServer.tool(
|
|
767
|
+
"ts_module_boundary",
|
|
768
|
+
"Analyze the boundary of a set of files: incoming/outgoing edges, shared dependencies, and an isolation score. Useful for understanding module coupling.",
|
|
769
|
+
{
|
|
770
|
+
files: z
|
|
771
|
+
.array(z.string())
|
|
772
|
+
.describe("Files defining the module boundary (relative or absolute paths)"),
|
|
773
|
+
},
|
|
774
|
+
async ({ files }) => {
|
|
775
|
+
const result = moduleBoundary(moduleGraph, files.map(absPath));
|
|
776
|
+
return {
|
|
777
|
+
content: [
|
|
778
|
+
{
|
|
779
|
+
type: "text" as const,
|
|
780
|
+
text: JSON.stringify({
|
|
781
|
+
internalEdges: result.internalEdges,
|
|
782
|
+
incomingEdges: result.incomingEdges.map((e) => ({
|
|
783
|
+
from: relPath(e.from),
|
|
784
|
+
to: relPath(e.to),
|
|
785
|
+
specifiers: e.specifiers,
|
|
786
|
+
})),
|
|
787
|
+
outgoingEdges: result.outgoingEdges.map((e) => ({
|
|
788
|
+
from: relPath(e.from),
|
|
789
|
+
to: relPath(e.to),
|
|
790
|
+
specifiers: e.specifiers,
|
|
791
|
+
})),
|
|
792
|
+
sharedDependencies: result.sharedDependencies.map(relPath),
|
|
793
|
+
isolationScore: Math.round(result.isolationScore * 1000) / 1000,
|
|
794
|
+
}),
|
|
795
|
+
},
|
|
796
|
+
],
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
);
|
|
800
|
+
|
|
801
|
+
// ─── Start ───────────────────────────────────────────────────────────────────
|
|
802
|
+
|
|
803
|
+
async function main() {
|
|
804
|
+
log("Starting TypeGraph MCP server...");
|
|
805
|
+
log(`Project root: ${projectRoot}`);
|
|
806
|
+
log(`tsconfig: ${tsconfigPath}`);
|
|
807
|
+
|
|
808
|
+
// Start tsserver and build module graph concurrently
|
|
809
|
+
const [, graphResult] = await Promise.all([
|
|
810
|
+
client.start(),
|
|
811
|
+
buildGraph(projectRoot, tsconfigPath),
|
|
812
|
+
]);
|
|
813
|
+
|
|
814
|
+
moduleGraph = graphResult.graph;
|
|
815
|
+
startWatcher(projectRoot, moduleGraph, graphResult.resolver);
|
|
816
|
+
|
|
817
|
+
const transport = new StdioServerTransport();
|
|
818
|
+
await mcpServer.connect(transport);
|
|
819
|
+
|
|
820
|
+
log("MCP server connected and ready");
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Graceful shutdown
|
|
824
|
+
process.on("SIGINT", () => {
|
|
825
|
+
log("Shutting down...");
|
|
826
|
+
client.shutdown();
|
|
827
|
+
process.exit(0);
|
|
828
|
+
});
|
|
829
|
+
process.on("SIGTERM", () => {
|
|
830
|
+
client.shutdown();
|
|
831
|
+
process.exit(0);
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
main().catch((err) => {
|
|
835
|
+
log("Fatal error:", err);
|
|
836
|
+
process.exit(1);
|
|
837
|
+
});
|