typegraph-mcp 0.9.41 → 0.9.43

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/dist/benchmark.js CHANGED
@@ -201,6 +201,21 @@ var TsServerClient = class {
201
201
  this.sendNotification("open", { file: absPath });
202
202
  await new Promise((r) => setTimeout(r, 50));
203
203
  }
204
+ async reloadOpenFile(file) {
205
+ const absPath = this.resolvePath(file);
206
+ if (!this.openFiles.has(absPath)) return false;
207
+ await this.sendRequest("reload", {
208
+ file: absPath,
209
+ tmpfile: absPath
210
+ });
211
+ return true;
212
+ }
213
+ closeFile(file) {
214
+ const absPath = this.resolvePath(file);
215
+ if (!this.openFiles.delete(absPath)) return false;
216
+ this.sendNotification("close", { file: absPath });
217
+ return true;
218
+ }
204
219
  // ─── Public API ────────────────────────────────────────────────────────
205
220
  async definition(file, line, offset) {
206
221
  const absPath = this.resolvePath(file);
package/dist/check.js CHANGED
@@ -301,7 +301,7 @@ function removeFile(graph, filePath) {
301
301
  graph.reverse.delete(filePath);
302
302
  graph.files.delete(filePath);
303
303
  }
304
- function startWatcher(projectRoot, graph, resolver) {
304
+ function startWatcher(projectRoot, graph, resolver, hooks) {
305
305
  try {
306
306
  const watcher = fs.watch(
307
307
  projectRoot,
@@ -318,8 +318,10 @@ function startWatcher(projectRoot, graph, resolver) {
318
318
  const absPath = path2.resolve(projectRoot, filename);
319
319
  if (fs.existsSync(absPath)) {
320
320
  updateFile(graph, absPath, resolver, projectRoot);
321
+ void hooks?.onFileUpdated?.(absPath);
321
322
  } else {
322
323
  removeFile(graph, absPath);
324
+ void hooks?.onFileDeleted?.(absPath);
323
325
  }
324
326
  }
325
327
  );
package/dist/cli.js CHANGED
@@ -317,7 +317,7 @@ function removeFile(graph, filePath) {
317
317
  graph.reverse.delete(filePath);
318
318
  graph.files.delete(filePath);
319
319
  }
320
- function startWatcher(projectRoot3, graph, resolver) {
320
+ function startWatcher(projectRoot3, graph, resolver, hooks) {
321
321
  try {
322
322
  const watcher = fs.watch(
323
323
  projectRoot3,
@@ -334,8 +334,10 @@ function startWatcher(projectRoot3, graph, resolver) {
334
334
  const absPath2 = path2.resolve(projectRoot3, filename);
335
335
  if (fs.existsSync(absPath2)) {
336
336
  updateFile(graph, absPath2, resolver, projectRoot3);
337
+ void hooks?.onFileUpdated?.(absPath2);
337
338
  } else {
338
339
  removeFile(graph, absPath2);
340
+ void hooks?.onFileDeleted?.(absPath2);
339
341
  }
340
342
  }
341
343
  );
@@ -1087,6 +1089,21 @@ var init_tsserver_client = __esm({
1087
1089
  this.sendNotification("open", { file: absPath2 });
1088
1090
  await new Promise((r) => setTimeout(r, 50));
1089
1091
  }
1092
+ async reloadOpenFile(file) {
1093
+ const absPath2 = this.resolvePath(file);
1094
+ if (!this.openFiles.has(absPath2)) return false;
1095
+ await this.sendRequest("reload", {
1096
+ file: absPath2,
1097
+ tmpfile: absPath2
1098
+ });
1099
+ return true;
1100
+ }
1101
+ closeFile(file) {
1102
+ const absPath2 = this.resolvePath(file);
1103
+ if (!this.openFiles.delete(absPath2)) return false;
1104
+ this.sendNotification("close", { file: absPath2 });
1105
+ return true;
1106
+ }
1090
1107
  // ─── Public API ────────────────────────────────────────────────────────
1091
1108
  async definition(file, line, offset) {
1092
1109
  const absPath2 = this.resolvePath(file);
@@ -2638,7 +2655,14 @@ async function main4() {
2638
2655
  ]);
2639
2656
  moduleGraph = graphResult.graph;
2640
2657
  moduleResolver = graphResult.resolver;
2641
- startWatcher(projectRoot2, moduleGraph, graphResult.resolver);
2658
+ startWatcher(projectRoot2, moduleGraph, graphResult.resolver, {
2659
+ onFileUpdated: (filePath) => client.reloadOpenFile(filePath).catch((err) => {
2660
+ log3(`Failed to reload open file ${relPath2(filePath)}:`, err);
2661
+ }),
2662
+ onFileDeleted: (filePath) => {
2663
+ client.closeFile(filePath);
2664
+ }
2665
+ });
2642
2666
  const transport = new StdioServerTransport();
2643
2667
  await mcpServer.connect(transport);
2644
2668
  log3("MCP server connected and ready");
@@ -297,7 +297,7 @@ function removeFile(graph, filePath) {
297
297
  graph.reverse.delete(filePath);
298
298
  graph.files.delete(filePath);
299
299
  }
300
- function startWatcher(projectRoot, graph, resolver) {
300
+ function startWatcher(projectRoot, graph, resolver, hooks) {
301
301
  try {
302
302
  const watcher = fs.watch(
303
303
  projectRoot,
@@ -314,8 +314,10 @@ function startWatcher(projectRoot, graph, resolver) {
314
314
  const absPath = path.resolve(projectRoot, filename);
315
315
  if (fs.existsSync(absPath)) {
316
316
  updateFile(graph, absPath, resolver, projectRoot);
317
+ void hooks?.onFileUpdated?.(absPath);
317
318
  } else {
318
319
  removeFile(graph, absPath);
320
+ void hooks?.onFileDeleted?.(absPath);
319
321
  }
320
322
  }
321
323
  );
package/dist/server.js CHANGED
@@ -202,6 +202,21 @@ var TsServerClient = class {
202
202
  this.sendNotification("open", { file: absPath2 });
203
203
  await new Promise((r) => setTimeout(r, 50));
204
204
  }
205
+ async reloadOpenFile(file) {
206
+ const absPath2 = this.resolvePath(file);
207
+ if (!this.openFiles.has(absPath2)) return false;
208
+ await this.sendRequest("reload", {
209
+ file: absPath2,
210
+ tmpfile: absPath2
211
+ });
212
+ return true;
213
+ }
214
+ closeFile(file) {
215
+ const absPath2 = this.resolvePath(file);
216
+ if (!this.openFiles.delete(absPath2)) return false;
217
+ this.sendNotification("close", { file: absPath2 });
218
+ return true;
219
+ }
205
220
  // ─── Public API ────────────────────────────────────────────────────────
206
221
  async definition(file, line, offset) {
207
222
  const absPath2 = this.resolvePath(file);
@@ -568,7 +583,7 @@ function removeFile(graph, filePath) {
568
583
  graph.reverse.delete(filePath);
569
584
  graph.files.delete(filePath);
570
585
  }
571
- function startWatcher(projectRoot2, graph, resolver) {
586
+ function startWatcher(projectRoot2, graph, resolver, hooks) {
572
587
  try {
573
588
  const watcher = fs2.watch(
574
589
  projectRoot2,
@@ -585,8 +600,10 @@ function startWatcher(projectRoot2, graph, resolver) {
585
600
  const absPath2 = path2.resolve(projectRoot2, filename);
586
601
  if (fs2.existsSync(absPath2)) {
587
602
  updateFile(graph, absPath2, resolver, projectRoot2);
603
+ void hooks?.onFileUpdated?.(absPath2);
588
604
  } else {
589
605
  removeFile(graph, absPath2);
606
+ void hooks?.onFileDeleted?.(absPath2);
590
607
  }
591
608
  }
592
609
  );
@@ -1677,7 +1694,14 @@ async function main() {
1677
1694
  ]);
1678
1695
  moduleGraph = graphResult.graph;
1679
1696
  moduleResolver = graphResult.resolver;
1680
- startWatcher(projectRoot, moduleGraph, graphResult.resolver);
1697
+ startWatcher(projectRoot, moduleGraph, graphResult.resolver, {
1698
+ onFileUpdated: (filePath) => client.reloadOpenFile(filePath).catch((err) => {
1699
+ log3(`Failed to reload open file ${relPath(filePath)}:`, err);
1700
+ }),
1701
+ onFileDeleted: (filePath) => {
1702
+ client.closeFile(filePath);
1703
+ }
1704
+ });
1681
1705
  const transport = new StdioServerTransport();
1682
1706
  await mcpServer.connect(transport);
1683
1707
  log3("MCP server connected and ready");
@@ -200,6 +200,21 @@ var TsServerClient = class {
200
200
  this.sendNotification("open", { file: absPath });
201
201
  await new Promise((r) => setTimeout(r, 50));
202
202
  }
203
+ async reloadOpenFile(file) {
204
+ const absPath = this.resolvePath(file);
205
+ if (!this.openFiles.has(absPath)) return false;
206
+ await this.sendRequest("reload", {
207
+ file: absPath,
208
+ tmpfile: absPath
209
+ });
210
+ return true;
211
+ }
212
+ closeFile(file) {
213
+ const absPath = this.resolvePath(file);
214
+ if (!this.openFiles.delete(absPath)) return false;
215
+ this.sendNotification("close", { file: absPath });
216
+ return true;
217
+ }
203
218
  // ─── Public API ────────────────────────────────────────────────────────
204
219
  async definition(file, line, offset) {
205
220
  const absPath = this.resolvePath(file);
@@ -195,6 +195,21 @@ var TsServerClient = class {
195
195
  this.sendNotification("open", { file: absPath });
196
196
  await new Promise((r) => setTimeout(r, 50));
197
197
  }
198
+ async reloadOpenFile(file) {
199
+ const absPath = this.resolvePath(file);
200
+ if (!this.openFiles.has(absPath)) return false;
201
+ await this.sendRequest("reload", {
202
+ file: absPath,
203
+ tmpfile: absPath
204
+ });
205
+ return true;
206
+ }
207
+ closeFile(file) {
208
+ const absPath = this.resolvePath(file);
209
+ if (!this.openFiles.delete(absPath)) return false;
210
+ this.sendNotification("close", { file: absPath });
211
+ return true;
212
+ }
198
213
  // ─── Public API ────────────────────────────────────────────────────────
199
214
  async definition(file, line, offset) {
200
215
  const absPath = this.resolvePath(file);
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env npx tsx
2
+
3
+ import * as assert from "node:assert/strict";
4
+ import * as fs from "node:fs";
5
+ import * as os from "node:os";
6
+ import * as path from "node:path";
7
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
8
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
9
+ import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js";
10
+
11
+ type DependencyTreeResult = {
12
+ root: string;
13
+ nodes: number;
14
+ files: string[];
15
+ };
16
+
17
+ type TypeInfoResult = {
18
+ type: string | null;
19
+ documentation: string | null;
20
+ kind?: string;
21
+ source?: string;
22
+ };
23
+
24
+ type ModuleExportsResult = {
25
+ file: string;
26
+ exports: Array<{
27
+ symbol: string;
28
+ type: string | null;
29
+ }>;
30
+ count: number;
31
+ };
32
+
33
+ function normalize(file: string): string {
34
+ return file.replaceAll("\\", "/");
35
+ }
36
+
37
+ function sleep(ms: number): Promise<void> {
38
+ return new Promise((resolve) => setTimeout(resolve, ms));
39
+ }
40
+
41
+ async function waitFor(
42
+ description: string,
43
+ fn: () => Promise<void>,
44
+ timeoutMs = 5_000,
45
+ intervalMs = 50
46
+ ): Promise<void> {
47
+ const start = Date.now();
48
+ let lastError: unknown;
49
+
50
+ while (Date.now() - start < timeoutMs) {
51
+ try {
52
+ await fn();
53
+ return;
54
+ } catch (err) {
55
+ lastError = err;
56
+ await sleep(intervalMs);
57
+ }
58
+ }
59
+
60
+ throw new Error(
61
+ `${description} did not stabilize within ${timeoutMs}ms: ${String(lastError)}`
62
+ );
63
+ }
64
+
65
+ function writeFile(root: string, relativePath: string, content: string): void {
66
+ const absPath = path.join(root, relativePath);
67
+ fs.mkdirSync(path.dirname(absPath), { recursive: true });
68
+ fs.writeFileSync(absPath, content);
69
+ }
70
+
71
+ async function main(): Promise<void> {
72
+ const repoRoot = import.meta.dirname;
73
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "typegraph-engine-sync-"));
74
+ const projectRoot = path.join(tempRoot, "project");
75
+
76
+ fs.mkdirSync(projectRoot, { recursive: true });
77
+ writeFile(
78
+ projectRoot,
79
+ "package.json",
80
+ JSON.stringify(
81
+ {
82
+ name: "typegraph-engine-sync-fixture",
83
+ private: true,
84
+ type: "module",
85
+ },
86
+ null,
87
+ 2
88
+ ) + "\n"
89
+ );
90
+ writeFile(
91
+ projectRoot,
92
+ "tsconfig.json",
93
+ JSON.stringify(
94
+ {
95
+ compilerOptions: {
96
+ target: "ES2022",
97
+ module: "ESNext",
98
+ moduleResolution: "Bundler",
99
+ strict: true,
100
+ },
101
+ include: ["src/**/*.ts"],
102
+ },
103
+ null,
104
+ 2
105
+ ) + "\n"
106
+ );
107
+
108
+ writeFile(projectRoot, "src/a.ts", 'export const current = "a" as const;\n');
109
+ writeFile(projectRoot, "src/b.ts", 'export const current = "b" as const;\n');
110
+ writeFile(
111
+ projectRoot,
112
+ "src/main.ts",
113
+ 'import { current } from "./a";\nexport const value = current;\n'
114
+ );
115
+ writeFile(projectRoot, "src/test.ts", "export const oldName = 1 as const;\n");
116
+ writeFile(projectRoot, "src/util.ts", "export const helper = 1 as const;\n");
117
+
118
+ fs.mkdirSync(path.join(projectRoot, "node_modules"), { recursive: true });
119
+ fs.symlinkSync(
120
+ path.join(repoRoot, "node_modules/typescript"),
121
+ path.join(projectRoot, "node_modules/typescript"),
122
+ "dir"
123
+ );
124
+
125
+ const client = new Client({ name: "engine-sync-test", version: "1.0.0" });
126
+ const transport = new StdioClientTransport({
127
+ command: path.join(repoRoot, "node_modules/.bin/tsx"),
128
+ args: [path.join(repoRoot, "server.ts")],
129
+ cwd: projectRoot,
130
+ env: {
131
+ TYPEGRAPH_PROJECT_ROOT: projectRoot,
132
+ TYPEGRAPH_TSCONFIG: path.join(projectRoot, "tsconfig.json"),
133
+ },
134
+ });
135
+
136
+ async function callTool<T>(name: string, args: Record<string, unknown>): Promise<T> {
137
+ const result = await client.request(
138
+ {
139
+ method: "tools/call",
140
+ params: {
141
+ name,
142
+ arguments: args,
143
+ },
144
+ },
145
+ CallToolResultSchema
146
+ );
147
+
148
+ const content = result.content[0];
149
+ assert.ok(content?.type === "text", `Expected text response from ${name}`);
150
+ return JSON.parse(content.text) as T;
151
+ }
152
+
153
+ try {
154
+ await client.connect(transport);
155
+
156
+ const initialType = await callTool<TypeInfoResult>("ts_type_info", {
157
+ file: "src/main.ts",
158
+ line: 2,
159
+ column: 14,
160
+ });
161
+ assert.match(initialType.type ?? "", /"a"/);
162
+
163
+ writeFile(projectRoot, "src/main.ts", 'import { current } from "./b";\nexport const value = current;\n');
164
+
165
+ await waitFor("import swap to synchronize graph and tsserver", async () => {
166
+ const deps = await callTool<DependencyTreeResult>("ts_dependency_tree", {
167
+ file: "src/main.ts",
168
+ });
169
+ const normalizedDeps = deps.files.map(normalize);
170
+ assert.ok(normalizedDeps.includes("src/b.ts"), `Expected src/b.ts in ${normalizedDeps}`);
171
+ assert.ok(!normalizedDeps.includes("src/a.ts"), `Expected src/a.ts to be removed from ${normalizedDeps}`);
172
+
173
+ const typeInfo = await callTool<TypeInfoResult>("ts_type_info", {
174
+ file: "src/main.ts",
175
+ line: 2,
176
+ column: 14,
177
+ });
178
+ assert.match(typeInfo.type ?? "", /"b"/);
179
+ });
180
+
181
+ // Open test.ts through ts_module_exports, then rename the export on disk.
182
+ const initialExports = await callTool<ModuleExportsResult>("ts_module_exports", {
183
+ file: "src/test.ts",
184
+ });
185
+ assert.ok(initialExports.exports.some((item) => item.symbol === "oldName"));
186
+
187
+ writeFile(projectRoot, "src/test.ts", "export const newName = 1 as const;\n");
188
+
189
+ await waitFor("symbol rename to refresh mixed export metadata", async () => {
190
+ const exportsResult = await callTool<ModuleExportsResult>("ts_module_exports", {
191
+ file: "src/test.ts",
192
+ });
193
+ const next = exportsResult.exports.find((item) => item.symbol === "newName");
194
+ assert.ok(next, `Expected newName in ${JSON.stringify(exportsResult.exports)}`);
195
+ assert.match(next.type ?? "", /\bnewName\b/);
196
+ assert.ok(!exportsResult.exports.some((item) => item.symbol === "oldName"));
197
+ });
198
+
199
+ // Open util.ts directly so tsserver tracks it, then delete it from disk.
200
+ const utilInfo = await callTool<TypeInfoResult>("ts_type_info", {
201
+ file: "src/util.ts",
202
+ line: 1,
203
+ column: 14,
204
+ });
205
+ assert.match(utilInfo.type ?? "", /\bhelper: 1\b/);
206
+ fs.rmSync(path.join(projectRoot, "src/util.ts"));
207
+
208
+ await waitFor("deleted file to disappear from semantic answers", async () => {
209
+ const deletedInfo = await callTool<TypeInfoResult>("ts_type_info", {
210
+ file: "src/util.ts",
211
+ line: 1,
212
+ column: 14,
213
+ });
214
+ assert.equal(
215
+ deletedInfo.type,
216
+ null,
217
+ `Expected deleted file to have no type info, got ${JSON.stringify(deletedInfo)}`
218
+ );
219
+ });
220
+
221
+ console.log("");
222
+ console.log("typegraph-mcp Engine Sync Test");
223
+ console.log("==============================");
224
+ console.log(" ✓ import swaps keep dependency_tree and type_info aligned");
225
+ console.log(" ✓ export renames refresh ts_module_exports semantic metadata");
226
+ console.log(" ✓ deleted open files do not survive as tsserver ghost snapshots");
227
+ } finally {
228
+ await transport.close().catch(() => {});
229
+ fs.rmSync(tempRoot, { recursive: true, force: true });
230
+ }
231
+ }
232
+
233
+ main().catch((err) => {
234
+ console.error(err);
235
+ process.exit(1);
236
+ });
package/module-graph.ts CHANGED
@@ -460,7 +460,11 @@ export function removeFile(graph: ModuleGraph, filePath: string): void {
460
460
  export function startWatcher(
461
461
  projectRoot: string,
462
462
  graph: ModuleGraph,
463
- resolver: ResolverFactory
463
+ resolver: ResolverFactory,
464
+ hooks?: {
465
+ onFileUpdated?: (filePath: string) => void | Promise<void>;
466
+ onFileDeleted?: (filePath: string) => void | Promise<void>;
467
+ }
464
468
  ): void {
465
469
  try {
466
470
  const watcher = fs.watch(
@@ -489,9 +493,11 @@ export function startWatcher(
489
493
  if (fs.existsSync(absPath)) {
490
494
  // File created or modified
491
495
  updateFile(graph, absPath, resolver, projectRoot);
496
+ void hooks?.onFileUpdated?.(absPath);
492
497
  } else {
493
498
  // File deleted
494
499
  removeFile(graph, absPath);
500
+ void hooks?.onFileDeleted?.(absPath);
495
501
  }
496
502
  }
497
503
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typegraph-mcp",
3
- "version": "0.9.41",
3
+ "version": "0.9.43",
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 && tsx export-surface-test.ts && tsx install-oxlint-test.ts",
37
+ "test": "tsx smoke-test.ts && tsx export-surface-test.ts && tsx engine-sync-test.ts && tsx install-oxlint-test.ts",
38
38
  "check": "tsx check.ts"
39
39
  },
40
40
  "dependencies": {
package/server.ts CHANGED
@@ -1132,7 +1132,15 @@ async function main() {
1132
1132
 
1133
1133
  moduleGraph = graphResult.graph;
1134
1134
  moduleResolver = graphResult.resolver;
1135
- startWatcher(projectRoot, moduleGraph, graphResult.resolver);
1135
+ startWatcher(projectRoot, moduleGraph, graphResult.resolver, {
1136
+ onFileUpdated: (filePath) =>
1137
+ client.reloadOpenFile(filePath).catch((err) => {
1138
+ log(`Failed to reload open file ${relPath(filePath)}:`, err);
1139
+ }),
1140
+ onFileDeleted: (filePath) => {
1141
+ client.closeFile(filePath);
1142
+ },
1143
+ });
1136
1144
 
1137
1145
  const transport = new StdioServerTransport();
1138
1146
  await mcpServer.connect(transport);
@@ -323,6 +323,23 @@ export class TsServerClient {
323
323
  await new Promise((r) => setTimeout(r, 50));
324
324
  }
325
325
 
326
+ async reloadOpenFile(file: string): Promise<boolean> {
327
+ const absPath = this.resolvePath(file);
328
+ if (!this.openFiles.has(absPath)) return false;
329
+ await this.sendRequest("reload", {
330
+ file: absPath,
331
+ tmpfile: absPath,
332
+ });
333
+ return true;
334
+ }
335
+
336
+ closeFile(file: string): boolean {
337
+ const absPath = this.resolvePath(file);
338
+ if (!this.openFiles.delete(absPath)) return false;
339
+ this.sendNotification("close", { file: absPath });
340
+ return true;
341
+ }
342
+
326
343
  // ─── Public API ────────────────────────────────────────────────────────
327
344
 
328
345
  async definition(file: string, line: number, offset: number): Promise<DefinitionResult[]> {