typegraph-mcp 0.9.37 → 0.9.39
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/check.ts +55 -16
- package/cli.ts +131 -46
- package/dist/benchmark.js +2 -2
- package/dist/check.js +48 -18
- package/dist/cli.js +419 -97
- package/dist/module-graph.js +4 -3
- package/dist/server.js +261 -43
- package/dist/smoke-test.js +2 -2
- package/export-surface-test.ts +202 -0
- package/install-oxlint-test.ts +95 -0
- package/module-graph.ts +3 -3
- package/package.json +2 -2
- package/server.ts +375 -55
- package/skills/code-exploration/SKILL.md +7 -0
- package/skills/deep-survey/SKILL.md +4 -0
- package/skills/tool-selection/SKILL.md +3 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
|
|
3
|
+
import * as assert from "node:assert/strict";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as os from "node:os";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
|
|
9
|
+
function copyDir(src: string, dest: string): void {
|
|
10
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
11
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
12
|
+
const srcPath = path.join(src, entry.name);
|
|
13
|
+
const destPath = path.join(dest, entry.name);
|
|
14
|
+
if (entry.isDirectory()) {
|
|
15
|
+
copyDir(srcPath, destPath);
|
|
16
|
+
} else {
|
|
17
|
+
fs.copyFileSync(srcPath, destPath);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function runTsx(
|
|
23
|
+
toolRoot: string,
|
|
24
|
+
args: string[],
|
|
25
|
+
cwd: string
|
|
26
|
+
): string {
|
|
27
|
+
return execFileSync(path.join(toolRoot, "node_modules/.bin/tsx"), args, {
|
|
28
|
+
cwd,
|
|
29
|
+
encoding: "utf-8",
|
|
30
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
31
|
+
env: process.env,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function assertIncludes(text: string, expected: string): void {
|
|
36
|
+
assert.ok(
|
|
37
|
+
text.includes(expected),
|
|
38
|
+
`Expected output to include:\n${expected}\n\nActual output:\n${text}`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function main(): Promise<void> {
|
|
43
|
+
const repoRoot = import.meta.dirname;
|
|
44
|
+
const fixtureRoot = path.join(repoRoot, ".fixtures/install-oxlint");
|
|
45
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "typegraph-install-oxlint-"));
|
|
46
|
+
const projectRoot = path.join(tempRoot, "project");
|
|
47
|
+
|
|
48
|
+
copyDir(fixtureRoot, projectRoot);
|
|
49
|
+
fs.mkdirSync(path.join(projectRoot, "node_modules"), { recursive: true });
|
|
50
|
+
fs.symlinkSync(
|
|
51
|
+
path.join(repoRoot, "node_modules/typescript"),
|
|
52
|
+
path.join(projectRoot, "node_modules/typescript"),
|
|
53
|
+
"dir"
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const setupOutput = runTsx(repoRoot, [path.join(repoRoot, "cli.ts"), "setup", "--yes"], projectRoot);
|
|
58
|
+
const pluginRoot = path.join(projectRoot, "plugins/typegraph-mcp");
|
|
59
|
+
|
|
60
|
+
const tsconfig = fs.readFileSync(path.join(projectRoot, "tsconfig.json"), "utf-8");
|
|
61
|
+
const oxlint = fs.readFileSync(path.join(projectRoot, ".oxlintrc.json"), "utf-8");
|
|
62
|
+
|
|
63
|
+
assertIncludes(tsconfig, '"$schema": "http://json.schemastore.org/tsconfig"');
|
|
64
|
+
assertIncludes(tsconfig, '"exclude": ["plugins/**"]');
|
|
65
|
+
assertIncludes(oxlint, '"ignorePatterns": [');
|
|
66
|
+
assertIncludes(oxlint, '"plugins/**"');
|
|
67
|
+
assert.ok(fs.existsSync(path.join(pluginRoot, "cli.ts")), "Expected installed plugin CLI");
|
|
68
|
+
|
|
69
|
+
assertIncludes(setupOutput, 'Added "plugins/**" to tsconfig.json exclude');
|
|
70
|
+
assertIncludes(setupOutput, 'Added "plugins/**" to .oxlintrc.json ignorePatterns');
|
|
71
|
+
assertIncludes(setupOutput, "Oxlint ignores plugins/ (.oxlintrc.json)");
|
|
72
|
+
|
|
73
|
+
const checkOutput = runTsx(pluginRoot, [path.join(pluginRoot, "cli.ts"), "check"], projectRoot);
|
|
74
|
+
assertIncludes(checkOutput, "Oxlint ignores plugins/ (.oxlintrc.json)");
|
|
75
|
+
assert.ok(
|
|
76
|
+
!checkOutput.includes("Lint config check (no ESLint or Oxlint config found)"),
|
|
77
|
+
`Did not expect lint config detection to be skipped:\n${checkOutput}`
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
console.log("");
|
|
81
|
+
console.log("typegraph-mcp Install Oxlint Test");
|
|
82
|
+
console.log("=================================");
|
|
83
|
+
console.log(" ✓ tsconfig schema URL preserved during exclude patch");
|
|
84
|
+
console.log(" ✓ tsconfig exclude patch ignores unrelated plugins text");
|
|
85
|
+
console.log(" ✓ .oxlintrc.json patched with plugins ignore");
|
|
86
|
+
console.log(" ✓ installed plugin health check recognizes Oxlint config");
|
|
87
|
+
} finally {
|
|
88
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
main().catch((err) => {
|
|
93
|
+
console.error(err);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
});
|
package/module-graph.ts
CHANGED
|
@@ -216,7 +216,7 @@ function distToSource(resolvedPath: string, projectRoot: string): string {
|
|
|
216
216
|
return resolvedPath;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
function
|
|
219
|
+
export function resolveProjectImport(
|
|
220
220
|
resolver: ResolverFactory,
|
|
221
221
|
fromDir: string,
|
|
222
222
|
specifier: string,
|
|
@@ -287,7 +287,7 @@ function buildForwardEdges(
|
|
|
287
287
|
const fromDir = path.dirname(filePath);
|
|
288
288
|
|
|
289
289
|
for (const raw of rawImports) {
|
|
290
|
-
const target =
|
|
290
|
+
const target = resolveProjectImport(resolver, fromDir, raw.specifier, projectRoot);
|
|
291
291
|
if (target) {
|
|
292
292
|
edges.push({
|
|
293
293
|
target,
|
|
@@ -397,7 +397,7 @@ export function updateFile(
|
|
|
397
397
|
const fromDir = path.dirname(filePath);
|
|
398
398
|
const newEdges: ImportEdge[] = [];
|
|
399
399
|
for (const raw of rawImports) {
|
|
400
|
-
const target =
|
|
400
|
+
const target = resolveProjectImport(resolver, fromDir, raw.specifier, projectRoot);
|
|
401
401
|
if (target) {
|
|
402
402
|
newEdges.push({
|
|
403
403
|
target,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typegraph-mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.39",
|
|
4
4
|
"description": "Type-aware codebase navigation for AI coding agents — 14 MCP tools powered by tsserver + oxc",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"scripts": {
|
|
35
35
|
"build": "tsup",
|
|
36
36
|
"start": "tsx server.ts",
|
|
37
|
-
"test": "tsx smoke-test.ts",
|
|
37
|
+
"test": "tsx smoke-test.ts && tsx export-surface-test.ts && tsx install-oxlint-test.ts",
|
|
38
38
|
"check": "tsx check.ts"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
package/server.ts
CHANGED
|
@@ -17,9 +17,16 @@
|
|
|
17
17
|
|
|
18
18
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
19
19
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
20
|
+
import { parseSync } from "oxc-parser";
|
|
21
|
+
import type { ResolverFactory } from "oxc-resolver";
|
|
20
22
|
import { z } from "zod";
|
|
21
23
|
import { TsServerClient, type NavBarItem } from "./tsserver-client.js";
|
|
22
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
buildGraph,
|
|
26
|
+
resolveProjectImport,
|
|
27
|
+
startWatcher,
|
|
28
|
+
type ModuleGraph,
|
|
29
|
+
} from "./module-graph.js";
|
|
23
30
|
import {
|
|
24
31
|
dependencyTree,
|
|
25
32
|
dependents,
|
|
@@ -44,6 +51,7 @@ const client = new TsServerClient(projectRoot, tsconfigPath);
|
|
|
44
51
|
|
|
45
52
|
// Module graph — initialized in main(), used by graph tools
|
|
46
53
|
let moduleGraph: ModuleGraph;
|
|
54
|
+
let moduleResolver: ResolverFactory;
|
|
47
55
|
|
|
48
56
|
const mcpServer = new McpServer({
|
|
49
57
|
name: "typegraph",
|
|
@@ -167,6 +175,347 @@ async function resolveParams(params: {
|
|
|
167
175
|
return { error: "Either line+column or symbol must be provided" };
|
|
168
176
|
}
|
|
169
177
|
|
|
178
|
+
type ModuleExportRecord = {
|
|
179
|
+
symbol: string;
|
|
180
|
+
kind: string;
|
|
181
|
+
line: number;
|
|
182
|
+
type: string | null;
|
|
183
|
+
exportKind: "value" | "type";
|
|
184
|
+
isTypeOnly: boolean;
|
|
185
|
+
isNamespace: boolean;
|
|
186
|
+
source: "local" | "re-export" | "star-re-export";
|
|
187
|
+
from: string | null;
|
|
188
|
+
definedIn: string;
|
|
189
|
+
definedLine: number | null;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
type StaticExportEntry = ReturnType<typeof parseSync>["module"]["staticExports"][number]["entries"][number];
|
|
193
|
+
|
|
194
|
+
const exportKinds = new Set([
|
|
195
|
+
"function",
|
|
196
|
+
"const",
|
|
197
|
+
"class",
|
|
198
|
+
"interface",
|
|
199
|
+
"type",
|
|
200
|
+
"enum",
|
|
201
|
+
"var",
|
|
202
|
+
"let",
|
|
203
|
+
"method",
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
function exportPriority(source: ModuleExportRecord["source"]): number {
|
|
207
|
+
switch (source) {
|
|
208
|
+
case "local":
|
|
209
|
+
return 3;
|
|
210
|
+
case "re-export":
|
|
211
|
+
return 2;
|
|
212
|
+
case "star-re-export":
|
|
213
|
+
return 1;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function exportKey(item: Pick<ModuleExportRecord, "symbol" | "exportKind">): string {
|
|
218
|
+
return `${item.symbol}:${item.exportKind}`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function sameExportOrigin(a: ModuleExportRecord, b: ModuleExportRecord): boolean {
|
|
222
|
+
return (
|
|
223
|
+
a.symbol === b.symbol &&
|
|
224
|
+
a.exportKind === b.exportKind &&
|
|
225
|
+
a.from === b.from &&
|
|
226
|
+
a.definedIn === b.definedIn &&
|
|
227
|
+
a.definedLine === b.definedLine
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function kindImpliesTypeOnly(kind: string): boolean {
|
|
232
|
+
return kind === "type" || kind === "interface";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function normalizeExportKindLabel(
|
|
236
|
+
kind: string,
|
|
237
|
+
exportKind: ModuleExportRecord["exportKind"]
|
|
238
|
+
): string {
|
|
239
|
+
if (exportKind === "type" && !kindImpliesTypeOnly(kind)) {
|
|
240
|
+
return "type";
|
|
241
|
+
}
|
|
242
|
+
return kind;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function upsertExport(
|
|
246
|
+
map: Map<string, ModuleExportRecord>,
|
|
247
|
+
conflicts: Set<string>,
|
|
248
|
+
nextExport: ModuleExportRecord
|
|
249
|
+
): void {
|
|
250
|
+
const key = exportKey(nextExport);
|
|
251
|
+
if (conflicts.has(key)) {
|
|
252
|
+
if (nextExport.source === "star-re-export") return;
|
|
253
|
+
conflicts.delete(key);
|
|
254
|
+
map.set(key, nextExport);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const existing = map.get(key);
|
|
259
|
+
if (
|
|
260
|
+
existing &&
|
|
261
|
+
existing.source === "star-re-export" &&
|
|
262
|
+
nextExport.source === "star-re-export" &&
|
|
263
|
+
!sameExportOrigin(existing, nextExport)
|
|
264
|
+
) {
|
|
265
|
+
map.delete(key);
|
|
266
|
+
conflicts.add(key);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (!existing || exportPriority(nextExport.source) > exportPriority(existing.source)) {
|
|
271
|
+
map.set(key, nextExport);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function offsetToLineColumn(source: string, offset: number | null | undefined): {
|
|
276
|
+
line: number;
|
|
277
|
+
column: number;
|
|
278
|
+
} {
|
|
279
|
+
const safeOffset = Math.max(0, Math.min(offset ?? 0, source.length));
|
|
280
|
+
const prefix = source.slice(0, safeOffset);
|
|
281
|
+
const lines = prefix.split("\n");
|
|
282
|
+
return {
|
|
283
|
+
line: lines.length,
|
|
284
|
+
column: (lines.at(-1)?.length ?? 0) + 1,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function normalizeExistingPath(filePath: string): string {
|
|
289
|
+
const resolved = path.resolve(filePath);
|
|
290
|
+
try {
|
|
291
|
+
return fs.realpathSync.native(resolved);
|
|
292
|
+
} catch {
|
|
293
|
+
return resolved;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const normalizedProjectRoot = normalizeExistingPath(projectRoot);
|
|
298
|
+
|
|
299
|
+
function projectPath(file: string): string {
|
|
300
|
+
return path.isAbsolute(file) ? relPath(file) : file;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function exportSymbol(entry: StaticExportEntry): string | null {
|
|
304
|
+
if (entry.exportName.kind === "Default") return "default";
|
|
305
|
+
return entry.exportName.name ?? entry.localName.name ?? entry.importName.name;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function exportLookupOffset(entry: StaticExportEntry): number | null | undefined {
|
|
309
|
+
if ((entry as { moduleRequest?: { value: string } }).moduleRequest) {
|
|
310
|
+
return entry.importName.start ?? entry.exportName.start ?? entry.start;
|
|
311
|
+
}
|
|
312
|
+
if (entry.exportName.kind === "Default") {
|
|
313
|
+
return entry.localName.start ?? entry.exportName.start ?? entry.start;
|
|
314
|
+
}
|
|
315
|
+
return entry.exportName.start ?? entry.localName.start ?? entry.start;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function resolveExportMetadata(
|
|
319
|
+
file: string,
|
|
320
|
+
line: number,
|
|
321
|
+
column: number,
|
|
322
|
+
fallbackKind: string
|
|
323
|
+
): Promise<{
|
|
324
|
+
kind: string;
|
|
325
|
+
type: string | null;
|
|
326
|
+
definedIn: string;
|
|
327
|
+
definedLine: number | null;
|
|
328
|
+
}> {
|
|
329
|
+
const defs = await client.definition(file, line, column);
|
|
330
|
+
const def = defs[0] ?? null;
|
|
331
|
+
|
|
332
|
+
let info = await client.quickinfo(file, line, column);
|
|
333
|
+
if ((!info || info.kind === "alias") && def) {
|
|
334
|
+
info = (await client.quickinfo(def.file, def.start.line, def.start.offset)) ?? info;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
kind: info?.kind ?? fallbackKind,
|
|
339
|
+
type: info?.displayString ?? null,
|
|
340
|
+
definedIn: projectPath(def?.file ?? file),
|
|
341
|
+
definedLine: def?.start.line ?? null,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function getModuleExports(
|
|
346
|
+
file: string,
|
|
347
|
+
visited = new Set<string>()
|
|
348
|
+
): Promise<ModuleExportRecord[]> {
|
|
349
|
+
const relFile = path.isAbsolute(file) ? relPath(file) : file;
|
|
350
|
+
const absFile = normalizeExistingPath(client.resolvePath(relFile));
|
|
351
|
+
if (visited.has(absFile)) return [];
|
|
352
|
+
|
|
353
|
+
const nextVisited = new Set(visited);
|
|
354
|
+
nextVisited.add(absFile);
|
|
355
|
+
|
|
356
|
+
const exportMap = new Map<string, ModuleExportRecord>();
|
|
357
|
+
const conflictingStarExports = new Set<string>();
|
|
358
|
+
|
|
359
|
+
let source: string;
|
|
360
|
+
try {
|
|
361
|
+
source = fs.readFileSync(absFile, "utf-8");
|
|
362
|
+
} catch {
|
|
363
|
+
return [...exportMap.values()];
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
let parsed: ReturnType<typeof parseSync>;
|
|
367
|
+
try {
|
|
368
|
+
parsed = parseSync(absFile, source);
|
|
369
|
+
} catch {
|
|
370
|
+
return [...exportMap.values()];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
for (const exp of parsed.module.staticExports) {
|
|
374
|
+
for (const entry of exp.entries) {
|
|
375
|
+
const moduleRequest = (entry as { moduleRequest?: { value: string } }).moduleRequest;
|
|
376
|
+
if (!moduleRequest) continue;
|
|
377
|
+
|
|
378
|
+
const targetFile = resolveProjectImport(
|
|
379
|
+
moduleResolver,
|
|
380
|
+
path.dirname(absFile),
|
|
381
|
+
moduleRequest.value,
|
|
382
|
+
projectRoot
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
const exportLoc = offsetToLineColumn(
|
|
386
|
+
source,
|
|
387
|
+
entry.exportName.start ?? entry.localName.start ?? entry.importName.start ?? entry.start
|
|
388
|
+
);
|
|
389
|
+
const importKind = entry.importName.kind as string;
|
|
390
|
+
const exportKind = entry.exportName.kind as string;
|
|
391
|
+
|
|
392
|
+
if (importKind === "AllButDefault" && exportKind === "None") {
|
|
393
|
+
if (!targetFile) continue;
|
|
394
|
+
const nestedExports = await getModuleExports(targetFile, nextVisited);
|
|
395
|
+
for (const nested of nestedExports) {
|
|
396
|
+
if (nested.symbol === "default") continue;
|
|
397
|
+
const starExportKind: ModuleExportRecord["exportKind"] = entry.isType
|
|
398
|
+
? "type"
|
|
399
|
+
: nested.exportKind;
|
|
400
|
+
upsertExport(exportMap, conflictingStarExports, {
|
|
401
|
+
...nested,
|
|
402
|
+
line: exportLoc.line,
|
|
403
|
+
exportKind: starExportKind,
|
|
404
|
+
isTypeOnly: starExportKind === "type",
|
|
405
|
+
source: "star-re-export",
|
|
406
|
+
from: relPath(targetFile),
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const symbol = exportSymbol(entry);
|
|
413
|
+
if (!symbol) continue;
|
|
414
|
+
|
|
415
|
+
const importedSymbol =
|
|
416
|
+
importKind === "Default"
|
|
417
|
+
? "default"
|
|
418
|
+
: importKind === "Name"
|
|
419
|
+
? entry.importName.name
|
|
420
|
+
: null;
|
|
421
|
+
const nestedMatch =
|
|
422
|
+
targetFile && importedSymbol
|
|
423
|
+
? (await getModuleExports(targetFile, nextVisited)).find(
|
|
424
|
+
(item) => item.symbol === importedSymbol
|
|
425
|
+
) ?? null
|
|
426
|
+
: null;
|
|
427
|
+
|
|
428
|
+
const lookupLoc = offsetToLineColumn(
|
|
429
|
+
source,
|
|
430
|
+
exportLookupOffset(entry)
|
|
431
|
+
);
|
|
432
|
+
const metadata = await resolveExportMetadata(
|
|
433
|
+
relFile,
|
|
434
|
+
lookupLoc.line,
|
|
435
|
+
lookupLoc.column,
|
|
436
|
+
importKind === "All" ? "namespace" : "alias"
|
|
437
|
+
);
|
|
438
|
+
const resolvedExportKind: ModuleExportRecord["exportKind"] =
|
|
439
|
+
entry.isType ||
|
|
440
|
+
nestedMatch?.exportKind === "type" ||
|
|
441
|
+
kindImpliesTypeOnly(nestedMatch?.kind ?? metadata.kind)
|
|
442
|
+
? "type"
|
|
443
|
+
: "value";
|
|
444
|
+
const resolvedKind = normalizeExportKindLabel(
|
|
445
|
+
nestedMatch?.kind ?? metadata.kind,
|
|
446
|
+
resolvedExportKind
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
upsertExport(exportMap, conflictingStarExports, {
|
|
450
|
+
symbol,
|
|
451
|
+
kind: resolvedKind,
|
|
452
|
+
line: exportLoc.line,
|
|
453
|
+
type: nestedMatch?.type ?? metadata.type,
|
|
454
|
+
exportKind: resolvedExportKind,
|
|
455
|
+
isTypeOnly: resolvedExportKind === "type",
|
|
456
|
+
isNamespace: importKind === "All",
|
|
457
|
+
source: "re-export",
|
|
458
|
+
from: targetFile ? relPath(targetFile) : moduleRequest.value,
|
|
459
|
+
definedIn: nestedMatch?.definedIn ?? metadata.definedIn,
|
|
460
|
+
definedLine: nestedMatch?.definedLine ?? metadata.definedLine,
|
|
461
|
+
});
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
for (const entry of exp.entries) {
|
|
466
|
+
const moduleRequest = (entry as { moduleRequest?: { value: string } }).moduleRequest;
|
|
467
|
+
if (moduleRequest) continue;
|
|
468
|
+
|
|
469
|
+
const symbol = exportSymbol(entry);
|
|
470
|
+
if (!symbol) continue;
|
|
471
|
+
|
|
472
|
+
const exportLoc = offsetToLineColumn(
|
|
473
|
+
source,
|
|
474
|
+
entry.exportName.start ?? entry.localName.start ?? entry.start
|
|
475
|
+
);
|
|
476
|
+
const lookupLoc = offsetToLineColumn(source, exportLookupOffset(entry));
|
|
477
|
+
const metadata = await resolveExportMetadata(
|
|
478
|
+
relFile,
|
|
479
|
+
lookupLoc.line,
|
|
480
|
+
lookupLoc.column,
|
|
481
|
+
entry.isType ? "type" : "value"
|
|
482
|
+
);
|
|
483
|
+
const resolvedExportKind: ModuleExportRecord["exportKind"] =
|
|
484
|
+
entry.isType || kindImpliesTypeOnly(metadata.kind) ? "type" : "value";
|
|
485
|
+
const resolvedKind = normalizeExportKindLabel(metadata.kind, resolvedExportKind);
|
|
486
|
+
|
|
487
|
+
// Skip navbar/import alias noise — only keep actual exported declaration kinds.
|
|
488
|
+
if (
|
|
489
|
+
resolvedExportKind === "value" &&
|
|
490
|
+
symbol !== "default" &&
|
|
491
|
+
!exportKinds.has(resolvedKind) &&
|
|
492
|
+
resolvedKind !== "namespace" &&
|
|
493
|
+
resolvedKind !== "class"
|
|
494
|
+
) {
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
upsertExport(exportMap, conflictingStarExports, {
|
|
499
|
+
symbol,
|
|
500
|
+
kind: resolvedKind,
|
|
501
|
+
line: exportLoc.line,
|
|
502
|
+
type: metadata.type,
|
|
503
|
+
exportKind: resolvedExportKind,
|
|
504
|
+
isTypeOnly: resolvedExportKind === "type",
|
|
505
|
+
isNamespace: false,
|
|
506
|
+
source: "local",
|
|
507
|
+
from: null,
|
|
508
|
+
definedIn: relFile,
|
|
509
|
+
definedLine: resolvedExportKind === "type" ? exportLoc.line : metadata.definedLine,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return [...exportMap.values()].sort(
|
|
515
|
+
(a, b) => a.line - b.line || a.symbol.localeCompare(b.symbol)
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
170
519
|
// ─── Tool 1: ts_find_symbol ─────────────────────────────────────────────────
|
|
171
520
|
|
|
172
521
|
mcpServer.tool(
|
|
@@ -499,68 +848,38 @@ mcpServer.tool(
|
|
|
499
848
|
|
|
500
849
|
mcpServer.tool(
|
|
501
850
|
"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.",
|
|
851
|
+
"List all exported symbols from a module with their resolved types, including re-exports when possible. Gives an at-a-glance understanding of what a file provides.",
|
|
503
852
|
{
|
|
504
853
|
file: z.string().describe("File to inspect"),
|
|
505
854
|
},
|
|
506
855
|
async ({ file }) => {
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
}
|
|
856
|
+
const exports = await getModuleExports(file);
|
|
857
|
+
const localCount = exports.filter((item) => item.source === "local").length;
|
|
858
|
+
const reExportCount = exports.length - localCount;
|
|
859
|
+
const typeOnlyCount = exports.filter((item) => item.isTypeOnly).length;
|
|
860
|
+
const valueCount = exports.length - typeOnlyCount;
|
|
861
|
+
const namespaceExportCount = exports.filter((item) => item.isNamespace).length;
|
|
862
|
+
const hasLocalRuntimeExports = exports.some(
|
|
863
|
+
(item) => item.source === "local" && !item.isTypeOnly
|
|
864
|
+
);
|
|
865
|
+
const isPrimarilyBarrel = exports.length > 0 && localCount < reExportCount;
|
|
558
866
|
|
|
559
867
|
return {
|
|
560
868
|
content: [
|
|
561
869
|
{
|
|
562
870
|
type: "text" as const,
|
|
563
|
-
text: JSON.stringify({
|
|
871
|
+
text: JSON.stringify({
|
|
872
|
+
file,
|
|
873
|
+
exports,
|
|
874
|
+
count: exports.length,
|
|
875
|
+
localCount,
|
|
876
|
+
reExportCount,
|
|
877
|
+
typeOnlyCount,
|
|
878
|
+
valueCount,
|
|
879
|
+
namespaceExportCount,
|
|
880
|
+
hasLocalRuntimeExports,
|
|
881
|
+
isPrimarilyBarrel,
|
|
882
|
+
}),
|
|
564
883
|
},
|
|
565
884
|
],
|
|
566
885
|
};
|
|
@@ -571,7 +890,7 @@ mcpServer.tool(
|
|
|
571
890
|
|
|
572
891
|
/** Convert an absolute path to project-relative */
|
|
573
892
|
function relPath(absPath: string): string {
|
|
574
|
-
return path.relative(
|
|
893
|
+
return path.relative(normalizedProjectRoot, normalizeExistingPath(absPath));
|
|
575
894
|
}
|
|
576
895
|
|
|
577
896
|
/** Convert a relative or absolute path to absolute */
|
|
@@ -812,6 +1131,7 @@ async function main() {
|
|
|
812
1131
|
]);
|
|
813
1132
|
|
|
814
1133
|
moduleGraph = graphResult.graph;
|
|
1134
|
+
moduleResolver = graphResult.resolver;
|
|
815
1135
|
startWatcher(projectRoot, moduleGraph, graphResult.resolver);
|
|
816
1136
|
|
|
817
1137
|
const transport = new StdioServerTransport();
|
|
@@ -24,6 +24,11 @@ If you know the file:
|
|
|
24
24
|
- Call `ts_module_exports` to see what the file provides
|
|
25
25
|
- Call `ts_find_symbol` to locate a specific symbol within it
|
|
26
26
|
|
|
27
|
+
If `ts_module_exports` on a top-level `index.ts` is empty or mostly re-exports:
|
|
28
|
+
- Treat it as a barrel, not a dead end
|
|
29
|
+
- Pivot to a composition module such as an app entrypoint, router, handler, API module, or service composition root
|
|
30
|
+
- Use `ts_dependency_tree` on that composition module to get quick architectural context
|
|
31
|
+
|
|
27
32
|
### Step 2: Understand the Type
|
|
28
33
|
Call `ts_type_info` on the entry point symbol. This gives you the full type signature and documentation without reading the entire file.
|
|
29
34
|
|
|
@@ -33,6 +38,8 @@ Call `ts_trace_chain` to follow the definition chain from the entry point to the
|
|
|
33
38
|
### Step 4: Explore the Neighborhood
|
|
34
39
|
Call `ts_subgraph` with the key files discovered in step 3 to see the surrounding module structure. Use `direction: "both"` and `depth: 1` for immediate context.
|
|
35
40
|
|
|
41
|
+
For a fast system-level read, `ts_dependency_tree` on the composition module often gives a better first picture than reading a barrel file's exports.
|
|
42
|
+
|
|
36
43
|
### Step 5: Deep Dive Where Needed
|
|
37
44
|
Only now, read specific files at the lines identified by the tools. You have precise coordinates — no need to read entire files.
|
|
38
45
|
|
|
@@ -54,6 +54,8 @@ Glob: **/index.ts, **/main.ts, **/entry*.ts, **/worker*.ts, **/server.ts, **/app
|
|
|
54
54
|
|
|
55
55
|
Exclude `node_modules/` hits. The results are your starting nodes.
|
|
56
56
|
|
|
57
|
+
Prefer these composition modules over top-level barrel files for your first passes. Barrels often describe the public API surface, but entry points and composition roots reveal how the system is actually wired.
|
|
58
|
+
|
|
57
59
|
### 1b. Dependency tree from every entry point (depth 2)
|
|
58
60
|
|
|
59
61
|
For each entry point, run:
|
|
@@ -121,6 +123,8 @@ Record for each:
|
|
|
121
123
|
- **Export types** — classes, interfaces, types, constants, functions. A file that exports 8 interfaces is a contract definition. A file that exports 8 constants is a configuration. A file that exports a mix of class + error + test layer is a service module.
|
|
122
124
|
- **Naming patterns** — do exports follow consistent naming (`*Service`, `*Error`, `*Test`, `*Live`)? Consistency across files is evidence of intentional patterns.
|
|
123
125
|
|
|
126
|
+
If a file is a barrel and `ts_module_exports` is sparse or uninformative, do not stop there. Pivot to the concrete composition modules it fronts and use `ts_dependency_tree` or `ts_module_exports` there instead.
|
|
127
|
+
|
|
124
128
|
### 2c. Module boundaries — how coupled are directories?
|
|
125
129
|
|
|
126
130
|
Identify 3-5 directories that look like they should be self-contained modules (e.g., `services/billing/`, `providers/email/`, `middleware/`).
|
|
@@ -29,6 +29,8 @@ Use **ts_type_info** — returns the same info as hovering in VS Code. Includes
|
|
|
29
29
|
### "What are all the exports of this file?"
|
|
30
30
|
Use **ts_module_exports** — lists all exported symbols with their resolved types.
|
|
31
31
|
|
|
32
|
+
If the file is a top-level barrel (`index.ts`, re-export hub), the result may be sparse or unhelpful for architecture discovery. For quick project insight, prefer composition modules such as entrypoints, routers, handler modules, service composition roots, or API modules that wire concrete behavior together.
|
|
33
|
+
|
|
32
34
|
### "Where is X used?"
|
|
33
35
|
Use **ts_references** for all semantic references. Unlike grep, this returns only real code references, not string matches in comments or unrelated variables.
|
|
34
36
|
|
|
@@ -63,6 +65,7 @@ Use **ts_module_boundary** — analyzes incoming/outgoing edges, shared dependen
|
|
|
63
65
|
3. **Combine tools for workflows.** Impact analysis = ts_blast_radius + ts_dependents. Refactor safety = ts_trace_chain + ts_import_cycles.
|
|
64
66
|
4. **Graph queries are instant** (~0.1ms). Point queries are fast (~2-50ms). Don't hesitate to use them liberally.
|
|
65
67
|
5. **First query may be slow** (~2s) as tsserver warms up. All subsequent queries are fast.
|
|
68
|
+
6. **For fast architecture reads, start at composition modules, not barrels.** Barrels are useful for API shape, but entrypoints and composition roots tell you how the system is actually wired.
|
|
66
69
|
|
|
67
70
|
## Tool Reference
|
|
68
71
|
|