typegraph-mcp 0.9.34 → 0.9.36

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/README.md CHANGED
@@ -128,9 +128,13 @@ typegraph-mcp <command> [options]
128
128
  bench Run benchmarks (token, latency, accuracy)
129
129
  start Start the MCP server (stdin/stdout)
130
130
 
131
- --yes Skip prompts --help Show help
131
+ --yes Skip prompts
132
+ --clean-global-codex Also remove a stale global Codex MCP entry for this project
133
+ --help Show help
132
134
  ```
133
135
 
136
+ `remove` always cleans up project-local config. If it detects a legacy global `~/.codex/config.toml` entry that points at the current project, it will ask before removing it in interactive mode. In non-interactive mode, pass `--clean-global-codex` to allow that global cleanup.
137
+
134
138
  ## Troubleshooting
135
139
 
136
140
  Run the health check first — it catches most issues:
@@ -156,11 +160,13 @@ Add this to your project's `.codex/config.toml`:
156
160
 
157
161
  ```toml
158
162
  [mcp_servers.typegraph]
159
- command = "npx"
160
- args = ["tsx", "/absolute/path/to/your-project/plugins/typegraph-mcp/server.ts"]
163
+ command = "/absolute/path/to/your-project/plugins/typegraph-mcp/node_modules/.bin/tsx"
164
+ args = ["/absolute/path/to/your-project/plugins/typegraph-mcp/server.ts"]
161
165
  env = { TYPEGRAPH_PROJECT_ROOT = "/absolute/path/to/your-project", TYPEGRAPH_TSCONFIG = "/absolute/path/to/your-project/tsconfig.json" }
162
166
  ```
163
167
 
168
+ Using the plugin-local `tsx` binary avoids relying on `npx tsx` being resolvable when Codex launches the MCP server.
169
+
164
170
  Codex only loads project `.codex/config.toml` files for trusted projects. If needed, add this to `~/.codex/config.toml`:
165
171
 
166
172
  ```toml
package/check.ts CHANGED
@@ -112,11 +112,19 @@ function hasCodexTypegraphRegistration(content: string): boolean {
112
112
  return /\[mcp_servers\.typegraph\]/.test(content);
113
113
  }
114
114
 
115
+ function hasCodexTsxLauncher(content: string): boolean {
116
+ return (
117
+ /command\s*=\s*"[^"]*tsx(?:\.cmd)?"/.test(content) ||
118
+ /args\s*=\s*\[[\s\S]*"tsx"/.test(content)
119
+ );
120
+ }
121
+
115
122
  function hasCompleteCodexTypegraphRegistration(content: string): boolean {
116
123
  return (
117
124
  hasCodexTypegraphRegistration(content) &&
118
125
  /command\s*=\s*"[^"]+"/.test(content) &&
119
- /args\s*=\s*\[[\s\S]*"tsx"/.test(content) &&
126
+ /args\s*=\s*\[[\s\S]*\]/.test(content) &&
127
+ hasCodexTsxLauncher(content) &&
120
128
  /TYPEGRAPH_PROJECT_ROOT\s*=/.test(content) &&
121
129
  /TYPEGRAPH_TSCONFIG\s*=/.test(content)
122
130
  );
@@ -308,7 +316,8 @@ export async function main(configOverride?: TypegraphConfig): Promise<CheckResul
308
316
  const codexConfigPath = path.resolve(projectRoot, ".codex/config.toml");
309
317
  const hasSection = hasCodexTypegraphRegistration(projectCodexConfig);
310
318
  const hasCommand = /command\s*=\s*"[^"]+"/.test(projectCodexConfig);
311
- const hasArgs = /args\s*=\s*\[[\s\S]*"tsx"/.test(projectCodexConfig);
319
+ const hasArgs = /args\s*=\s*\[[\s\S]*\]/.test(projectCodexConfig);
320
+ const hasTsxLauncher = hasCodexTsxLauncher(projectCodexConfig);
312
321
  const hasEnvRoot = /TYPEGRAPH_PROJECT_ROOT\s*=/.test(projectCodexConfig);
313
322
  const hasEnvTsconfig = /TYPEGRAPH_TSCONFIG\s*=/.test(projectCodexConfig);
314
323
  if (hasProjectCodexRegistration) {
@@ -324,7 +333,8 @@ export async function main(configOverride?: TypegraphConfig): Promise<CheckResul
324
333
  const issues: string[] = [];
325
334
  if (!hasSection) issues.push("[mcp_servers.typegraph] section is missing");
326
335
  if (!hasCommand) issues.push("command is missing");
327
- if (!hasArgs) issues.push("args should include 'tsx'");
336
+ if (!hasArgs) issues.push("args are missing");
337
+ if (!hasTsxLauncher) issues.push("command should point to tsx or args should include 'tsx'");
328
338
  if (!hasEnvRoot) issues.push("TYPEGRAPH_PROJECT_ROOT is missing");
329
339
  if (!hasEnvTsconfig) issues.push("TYPEGRAPH_TSCONFIG is missing");
330
340
  fail(
package/cli.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  *
11
11
  * Options:
12
12
  * --yes Skip confirmation prompts (accept all defaults)
13
+ * --clean-global-codex Also remove a stale global Codex MCP entry for this project
13
14
  * --help Show help
14
15
  */
15
16
 
@@ -35,6 +36,17 @@ interface AgentDef {
35
36
  detect: (projectRoot: string) => boolean;
36
37
  }
37
38
 
39
+ interface LegacyGlobalCodexCleanup {
40
+ globalConfigPath: string;
41
+ nextContent: string;
42
+ }
43
+
44
+ interface RemovePluginOptions {
45
+ removeGlobalCodex: boolean;
46
+ legacyGlobalCodexCleanup: LegacyGlobalCodexCleanup | null;
47
+ warnAboutGlobalCodex: boolean;
48
+ }
49
+
38
50
  // ─── Constants ───────────────────────────────────────────────────────────────
39
51
 
40
52
  const AGENT_SNIPPET = `
@@ -163,8 +175,9 @@ Commands:
163
175
  start Start the MCP server (stdin/stdout)
164
176
 
165
177
  Options:
166
- --yes Skip confirmation prompts (accept all defaults)
167
- --help Show this help
178
+ --yes Skip confirmation prompts (accept all defaults)
179
+ --clean-global-codex Also remove a stale global Codex MCP entry for this project
180
+ --help Show this help
168
181
  `.trim();
169
182
 
170
183
  // ─── Helpers ─────────────────────────────────────────────────────────────────
@@ -207,12 +220,78 @@ function getAbsoluteMcpServerEntry(projectRoot: string): {
207
220
  };
208
221
  }
209
222
 
223
+ function getCodexMcpServerEntry(projectRoot: string): {
224
+ command: string;
225
+ args: string[];
226
+ env: Record<string, string>;
227
+ } {
228
+ return {
229
+ command: path.resolve(projectRoot, PLUGIN_DIR_NAME, "node_modules/.bin/tsx"),
230
+ args: [path.resolve(projectRoot, PLUGIN_DIR_NAME, "server.ts")],
231
+ env: {
232
+ TYPEGRAPH_PROJECT_ROOT: projectRoot,
233
+ TYPEGRAPH_TSCONFIG: path.resolve(projectRoot, "tsconfig.json"),
234
+ },
235
+ };
236
+ }
237
+
210
238
  function getCodexConfigPath(projectRoot: string): string {
211
239
  return path.resolve(projectRoot, ".codex/config.toml");
212
240
  }
213
241
 
214
- function hasCodexMcpSection(content: string): boolean {
215
- return content.includes("[mcp_servers.typegraph]");
242
+ function isTomlSectionGroup(sectionName: string | null, prefix: string): boolean {
243
+ return sectionName === prefix || sectionName?.startsWith(`${prefix}.`) === true;
244
+ }
245
+
246
+ function splitTomlBlocks(content: string): Array<{ sectionName: string | null; raw: string }> {
247
+ const lines = content.split(/\r?\n/);
248
+ const blocks: Array<{ sectionName: string | null; raw: string }> = [];
249
+ let sectionName: string | null = null;
250
+ let currentLines: string[] = [];
251
+
252
+ for (const line of lines) {
253
+ const match = line.match(/^\[([^\]]+)\]\s*$/);
254
+ if (match) {
255
+ if (currentLines.length > 0 || sectionName !== null) {
256
+ blocks.push({ sectionName, raw: currentLines.join("\n") });
257
+ }
258
+ sectionName = match[1]!;
259
+ currentLines = [line];
260
+ continue;
261
+ }
262
+
263
+ currentLines.push(line);
264
+ }
265
+
266
+ if (currentLines.length > 0 || sectionName !== null) {
267
+ blocks.push({ sectionName, raw: currentLines.join("\n") });
268
+ }
269
+
270
+ return blocks;
271
+ }
272
+
273
+ function removeTomlSectionGroup(
274
+ content: string,
275
+ prefix: string
276
+ ): { content: string; removed: boolean; removedContent: string } {
277
+ const blocks = splitTomlBlocks(content);
278
+ const removedBlocks = blocks.filter((block) => isTomlSectionGroup(block.sectionName, prefix));
279
+ if (removedBlocks.length === 0) {
280
+ return { content, removed: false, removedContent: "" };
281
+ }
282
+
283
+ const keptBlocks = blocks.filter((block) => !isTomlSectionGroup(block.sectionName, prefix));
284
+ const nextContent = keptBlocks
285
+ .map((block) => block.raw)
286
+ .join("\n")
287
+ .replace(/\n{3,}/g, "\n\n")
288
+ .trimEnd();
289
+
290
+ return {
291
+ content: nextContent ? `${nextContent}\n` : "",
292
+ removed: true,
293
+ removedContent: removedBlocks.map((block) => block.raw).join("\n").trim(),
294
+ };
216
295
  }
217
296
 
218
297
  function upsertCodexMcpSection(content: string, block: string): { content: string; changed: boolean } {
@@ -236,12 +315,13 @@ function upsertCodexMcpSection(content: string, block: string): { content: strin
236
315
  }
237
316
 
238
317
  function makeCodexMcpBlock(projectRoot: string): string {
239
- const absoluteEntry = getAbsoluteMcpServerEntry(projectRoot);
318
+ const absoluteEntry = getCodexMcpServerEntry(projectRoot);
319
+ const args = absoluteEntry.args.map((arg) => `"${arg}"`).join(", ");
240
320
  return [
241
321
  "",
242
322
  "[mcp_servers.typegraph]",
243
323
  `command = "${absoluteEntry.command}"`,
244
- `args = ["${absoluteEntry.args[0]}", "${absoluteEntry.args[1]}"]`,
324
+ `args = [${args}]`,
245
325
  `env = { TYPEGRAPH_PROJECT_ROOT = "${absoluteEntry.env.TYPEGRAPH_PROJECT_ROOT}", TYPEGRAPH_TSCONFIG = "${absoluteEntry.env.TYPEGRAPH_TSCONFIG}" }`,
246
326
  "",
247
327
  ].join("\n");
@@ -288,6 +368,92 @@ function isCodexProjectTrusted(projectRoot: string): boolean {
288
368
  return matchesTrustedProject();
289
369
  }
290
370
 
371
+ function pathEqualsOrContains(candidatePath: string, targetPath: string): boolean {
372
+ const resolvedCandidate = path.resolve(candidatePath);
373
+ const resolvedTarget = path.resolve(targetPath);
374
+ if (resolvedCandidate === resolvedTarget || resolvedCandidate.startsWith(`${resolvedTarget}${path.sep}`)) {
375
+ return true;
376
+ }
377
+
378
+ try {
379
+ const realCandidate = fs.realpathSync(candidatePath);
380
+ const realTarget = fs.realpathSync(targetPath);
381
+ return realCandidate === realTarget || realCandidate.startsWith(`${realTarget}${path.sep}`);
382
+ } catch {
383
+ return false;
384
+ }
385
+ }
386
+
387
+ function findLegacyGlobalCodexCleanup(projectRoot: string): LegacyGlobalCodexCleanup | null {
388
+ const home = process.env.HOME;
389
+ if (!home) return null;
390
+
391
+ const globalConfigPath = path.join(home, ".codex/config.toml");
392
+ if (!fs.existsSync(globalConfigPath)) return null;
393
+
394
+ const content = fs.readFileSync(globalConfigPath, "utf-8");
395
+ const { content: nextContent, removed, removedContent } = removeTomlSectionGroup(content, "mcp_servers.typegraph");
396
+ if (!removed) return null;
397
+
398
+ const pluginRoot = path.resolve(projectRoot, PLUGIN_DIR_NAME);
399
+ const quotedPaths = Array.from(removedContent.matchAll(/"([^"\n]+)"/g), (match) => match[1]!);
400
+ const looksProjectSpecific = quotedPaths.some((quotedPath) =>
401
+ pathEqualsOrContains(quotedPath, projectRoot) ||
402
+ pathEqualsOrContains(quotedPath, pluginRoot)
403
+ );
404
+
405
+ if (!looksProjectSpecific) {
406
+ return null;
407
+ }
408
+
409
+ return { globalConfigPath, nextContent };
410
+ }
411
+
412
+ function removeLegacyGlobalCodexMcp(cleanup: LegacyGlobalCodexCleanup): void {
413
+ if (cleanup.nextContent === "") {
414
+ fs.unlinkSync(cleanup.globalConfigPath);
415
+ } else {
416
+ fs.writeFileSync(cleanup.globalConfigPath, cleanup.nextContent);
417
+ }
418
+
419
+ p.log.info("~/.codex/config.toml: removed stale global typegraph MCP server entry for this project");
420
+ }
421
+
422
+ async function resolveRemovePluginOptions(
423
+ projectRoot: string,
424
+ yes: boolean,
425
+ cleanGlobalCodex: boolean
426
+ ): Promise<RemovePluginOptions> {
427
+ const legacyGlobalCodexCleanup = findLegacyGlobalCodexCleanup(projectRoot);
428
+ let removeGlobalCodex = cleanGlobalCodex;
429
+
430
+ if (legacyGlobalCodexCleanup && !cleanGlobalCodex && !yes) {
431
+ const shouldRemoveGlobal = await p.confirm({
432
+ message: "Also remove the stale global Codex MCP entry for this project from ~/.codex/config.toml?",
433
+ initialValue: false,
434
+ });
435
+ if (p.isCancel(shouldRemoveGlobal)) {
436
+ p.cancel("Removal cancelled.");
437
+ process.exit(0);
438
+ }
439
+ removeGlobalCodex = shouldRemoveGlobal;
440
+ }
441
+
442
+ return {
443
+ removeGlobalCodex,
444
+ legacyGlobalCodexCleanup,
445
+ warnAboutGlobalCodex: legacyGlobalCodexCleanup !== null && !removeGlobalCodex,
446
+ };
447
+ }
448
+
449
+ function warnAboutStaleGlobalCodex(): void {
450
+ p.log.warn(
451
+ "Left a stale global Codex MCP entry for this project in ~/.codex/config.toml. " +
452
+ "Codex may show MCP startup warnings or errors until you remove it. " +
453
+ "Re-run `typegraph-mcp remove --clean-global-codex` or remove the `typegraph` block manually."
454
+ );
455
+ }
456
+
291
457
  /** Register the typegraph MCP server in agent-specific config files */
292
458
  function registerMcpServers(projectRoot: string, selectedAgents: AgentId[]): void {
293
459
  if (selectedAgents.includes("cursor")) {
@@ -401,26 +567,19 @@ function registerCodexMcp(projectRoot: string): void {
401
567
  function deregisterCodexMcp(projectRoot: string): void {
402
568
  const configPath = ".codex/config.toml";
403
569
  const fullPath = getCodexConfigPath(projectRoot);
404
- if (!fs.existsSync(fullPath)) return;
405
-
406
- let content = fs.readFileSync(fullPath, "utf-8");
407
- if (!hasCodexMcpSection(content)) return;
408
-
409
- // Remove the [mcp_servers.typegraph] section (stops at next section header or end of file)
410
- content = content.replace(
411
- /\n?\[mcp_servers\.typegraph\]\n[\s\S]*?(?=\n\[|$)/,
412
- ""
413
- );
414
-
415
- // Clean up multiple trailing newlines
416
- content = content.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
417
-
418
- if (content.trim() === "") {
419
- fs.unlinkSync(fullPath);
420
- } else {
421
- fs.writeFileSync(fullPath, content);
570
+ if (fs.existsSync(fullPath)) {
571
+ const content = fs.readFileSync(fullPath, "utf-8");
572
+ const { content: nextContent, removed } = removeTomlSectionGroup(content, "mcp_servers.typegraph");
573
+
574
+ if (removed) {
575
+ if (nextContent === "") {
576
+ fs.unlinkSync(fullPath);
577
+ } else {
578
+ fs.writeFileSync(fullPath, nextContent);
579
+ }
580
+ p.log.info(`${configPath}: removed typegraph MCP server`);
581
+ }
422
582
  }
423
- p.log.info(`${configPath}: removed typegraph MCP server`);
424
583
  }
425
584
 
426
585
  // ─── TSConfig Exclude ─────────────────────────────────────────────────────────
@@ -606,7 +765,11 @@ async function setup(yes: boolean): Promise<void> {
606
765
  }
607
766
 
608
767
  if (action === "remove") {
609
- await removePlugin(projectRoot, targetDir);
768
+ const removeOptions = await resolveRemovePluginOptions(projectRoot, false, false);
769
+ await removePlugin(projectRoot, targetDir, removeOptions);
770
+ if (removeOptions.warnAboutGlobalCodex) {
771
+ warnAboutStaleGlobalCodex();
772
+ }
610
773
  return;
611
774
  }
612
775
 
@@ -748,16 +911,26 @@ async function setup(yes: boolean): Promise<void> {
748
911
 
749
912
  // ─── Remove Command ──────────────────────────────────────────────────────────
750
913
 
751
- async function removePlugin(projectRoot: string, pluginDir: string): Promise<void> {
914
+ async function removePlugin(
915
+ projectRoot: string,
916
+ pluginDir: string,
917
+ options: RemovePluginOptions
918
+ ): Promise<void> {
752
919
  const s = p.spinner();
753
920
  s.start("Removing typegraph-mcp...");
754
921
 
755
- // 1. Remove plugin directory
922
+ // 1. Deregister MCP server from agent config files while project paths still exist
923
+ deregisterMcpServers(projectRoot);
924
+ if (options.removeGlobalCodex && options.legacyGlobalCodexCleanup) {
925
+ removeLegacyGlobalCodexMcp(options.legacyGlobalCodexCleanup);
926
+ }
927
+
928
+ // 2. Remove plugin directory
756
929
  if (fs.existsSync(pluginDir)) {
757
930
  fs.rmSync(pluginDir, { recursive: true });
758
931
  }
759
932
 
760
- // 2. Remove .agents/skills/ entries (only typegraph-mcp skills, not the whole dir)
933
+ // 3. Remove .agents/skills/ entries (only typegraph-mcp skills, not the whole dir)
761
934
  const agentsSkillsDir = path.resolve(projectRoot, ".agents/skills");
762
935
  for (const skill of SKILL_NAMES) {
763
936
  const skillDir = path.join(agentsSkillsDir, skill);
@@ -774,7 +947,7 @@ async function removePlugin(projectRoot: string, pluginDir: string): Promise<voi
774
947
  }
775
948
  }
776
949
 
777
- // 3. Remove agent instruction snippet from all known agent files
950
+ // 4. Remove agent instruction snippet from all known agent files
778
951
  const allAgentFiles = AGENT_IDS
779
952
  .map((id) => AGENTS[id].agentFile)
780
953
  .filter((f): f is string => f !== null);
@@ -797,7 +970,7 @@ async function removePlugin(projectRoot: string, pluginDir: string): Promise<voi
797
970
  }
798
971
  }
799
972
 
800
- // 4. Remove --plugin-dir ./plugins/typegraph-mcp from CLAUDE.md
973
+ // 5. Remove --plugin-dir ./plugins/typegraph-mcp from CLAUDE.md
801
974
  const claudeMdPath = path.resolve(projectRoot, "CLAUDE.md");
802
975
  if (fs.existsSync(claudeMdPath)) {
803
976
  let content = fs.readFileSync(claudeMdPath, "utf-8");
@@ -807,9 +980,6 @@ async function removePlugin(projectRoot: string, pluginDir: string): Promise<voi
807
980
 
808
981
  s.stop("Removed typegraph-mcp");
809
982
 
810
- // 5. Deregister MCP server from agent config files
811
- deregisterMcpServers(projectRoot);
812
-
813
983
  p.outro("typegraph-mcp has been uninstalled from this project.");
814
984
  }
815
985
 
@@ -913,6 +1083,7 @@ async function runVerification(pluginDir: string, selectedAgents: AgentId[]): Pr
913
1083
  async function remove(yes: boolean): Promise<void> {
914
1084
  const projectRoot = process.cwd();
915
1085
  const pluginDir = path.resolve(projectRoot, PLUGIN_DIR_NAME);
1086
+ const cleanGlobalCodex = args.includes("--clean-global-codex");
916
1087
 
917
1088
  process.stdout.write("\x1Bc");
918
1089
  p.intro("TypeGraph MCP Remove");
@@ -930,7 +1101,12 @@ async function remove(yes: boolean): Promise<void> {
930
1101
  }
931
1102
  }
932
1103
 
933
- await removePlugin(projectRoot, pluginDir);
1104
+ const removeOptions = await resolveRemovePluginOptions(projectRoot, yes, cleanGlobalCodex);
1105
+ await removePlugin(projectRoot, pluginDir, removeOptions);
1106
+
1107
+ if (removeOptions.warnAboutGlobalCodex) {
1108
+ warnAboutStaleGlobalCodex();
1109
+ }
934
1110
  }
935
1111
 
936
1112
  // ─── Check Command ───────────────────────────────────────────────────────────
package/dist/check.js CHANGED
@@ -440,8 +440,11 @@ function readProjectCodexConfig(projectRoot) {
440
440
  function hasCodexTypegraphRegistration(content) {
441
441
  return /\[mcp_servers\.typegraph\]/.test(content);
442
442
  }
443
+ function hasCodexTsxLauncher(content) {
444
+ return /command\s*=\s*"[^"]*tsx(?:\.cmd)?"/.test(content) || /args\s*=\s*\[[\s\S]*"tsx"/.test(content);
445
+ }
443
446
  function hasCompleteCodexTypegraphRegistration(content) {
444
- return hasCodexTypegraphRegistration(content) && /command\s*=\s*"[^"]+"/.test(content) && /args\s*=\s*\[[\s\S]*"tsx"/.test(content) && /TYPEGRAPH_PROJECT_ROOT\s*=/.test(content) && /TYPEGRAPH_TSCONFIG\s*=/.test(content);
447
+ return hasCodexTypegraphRegistration(content) && /command\s*=\s*"[^"]+"/.test(content) && /args\s*=\s*\[[\s\S]*\]/.test(content) && hasCodexTsxLauncher(content) && /TYPEGRAPH_PROJECT_ROOT\s*=/.test(content) && /TYPEGRAPH_TSCONFIG\s*=/.test(content);
445
448
  }
446
449
  function hasTrustedCodexProject(projectRoot) {
447
450
  const home = process.env.HOME;
@@ -584,7 +587,8 @@ async function main(configOverride) {
584
587
  const codexConfigPath = path3.resolve(projectRoot, ".codex/config.toml");
585
588
  const hasSection = hasCodexTypegraphRegistration(projectCodexConfig);
586
589
  const hasCommand = /command\s*=\s*"[^"]+"/.test(projectCodexConfig);
587
- const hasArgs = /args\s*=\s*\[[\s\S]*"tsx"/.test(projectCodexConfig);
590
+ const hasArgs = /args\s*=\s*\[[\s\S]*\]/.test(projectCodexConfig);
591
+ const hasTsxLauncher = hasCodexTsxLauncher(projectCodexConfig);
588
592
  const hasEnvRoot = /TYPEGRAPH_PROJECT_ROOT\s*=/.test(projectCodexConfig);
589
593
  const hasEnvTsconfig = /TYPEGRAPH_TSCONFIG\s*=/.test(projectCodexConfig);
590
594
  if (hasProjectCodexRegistration) {
@@ -600,7 +604,8 @@ async function main(configOverride) {
600
604
  const issues = [];
601
605
  if (!hasSection) issues.push("[mcp_servers.typegraph] section is missing");
602
606
  if (!hasCommand) issues.push("command is missing");
603
- if (!hasArgs) issues.push("args should include 'tsx'");
607
+ if (!hasArgs) issues.push("args are missing");
608
+ if (!hasTsxLauncher) issues.push("command should point to tsx or args should include 'tsx'");
604
609
  if (!hasEnvRoot) issues.push("TYPEGRAPH_PROJECT_ROOT is missing");
605
610
  if (!hasEnvTsconfig) issues.push("TYPEGRAPH_TSCONFIG is missing");
606
611
  fail(
package/dist/cli.js CHANGED
@@ -447,8 +447,11 @@ function readProjectCodexConfig(projectRoot3) {
447
447
  function hasCodexTypegraphRegistration(content) {
448
448
  return /\[mcp_servers\.typegraph\]/.test(content);
449
449
  }
450
+ function hasCodexTsxLauncher(content) {
451
+ return /command\s*=\s*"[^"]*tsx(?:\.cmd)?"/.test(content) || /args\s*=\s*\[[\s\S]*"tsx"/.test(content);
452
+ }
450
453
  function hasCompleteCodexTypegraphRegistration(content) {
451
- return hasCodexTypegraphRegistration(content) && /command\s*=\s*"[^"]+"/.test(content) && /args\s*=\s*\[[\s\S]*"tsx"/.test(content) && /TYPEGRAPH_PROJECT_ROOT\s*=/.test(content) && /TYPEGRAPH_TSCONFIG\s*=/.test(content);
454
+ return hasCodexTypegraphRegistration(content) && /command\s*=\s*"[^"]+"/.test(content) && /args\s*=\s*\[[\s\S]*\]/.test(content) && hasCodexTsxLauncher(content) && /TYPEGRAPH_PROJECT_ROOT\s*=/.test(content) && /TYPEGRAPH_TSCONFIG\s*=/.test(content);
452
455
  }
453
456
  function hasTrustedCodexProject(projectRoot3) {
454
457
  const home = process.env.HOME;
@@ -591,7 +594,8 @@ async function main(configOverride) {
591
594
  const codexConfigPath = path3.resolve(projectRoot3, ".codex/config.toml");
592
595
  const hasSection = hasCodexTypegraphRegistration(projectCodexConfig);
593
596
  const hasCommand = /command\s*=\s*"[^"]+"/.test(projectCodexConfig);
594
- const hasArgs = /args\s*=\s*\[[\s\S]*"tsx"/.test(projectCodexConfig);
597
+ const hasArgs = /args\s*=\s*\[[\s\S]*\]/.test(projectCodexConfig);
598
+ const hasTsxLauncher = hasCodexTsxLauncher(projectCodexConfig);
595
599
  const hasEnvRoot = /TYPEGRAPH_PROJECT_ROOT\s*=/.test(projectCodexConfig);
596
600
  const hasEnvTsconfig = /TYPEGRAPH_TSCONFIG\s*=/.test(projectCodexConfig);
597
601
  if (hasProjectCodexRegistration) {
@@ -607,7 +611,8 @@ async function main(configOverride) {
607
611
  const issues = [];
608
612
  if (!hasSection) issues.push("[mcp_servers.typegraph] section is missing");
609
613
  if (!hasCommand) issues.push("command is missing");
610
- if (!hasArgs) issues.push("args should include 'tsx'");
614
+ if (!hasArgs) issues.push("args are missing");
615
+ if (!hasTsxLauncher) issues.push("command should point to tsx or args should include 'tsx'");
611
616
  if (!hasEnvRoot) issues.push("TYPEGRAPH_PROJECT_ROOT is missing");
612
617
  if (!hasEnvTsconfig) issues.push("TYPEGRAPH_TSCONFIG is missing");
613
618
  fail(
@@ -3024,8 +3029,9 @@ Commands:
3024
3029
  start Start the MCP server (stdin/stdout)
3025
3030
 
3026
3031
  Options:
3027
- --yes Skip confirmation prompts (accept all defaults)
3028
- --help Show this help
3032
+ --yes Skip confirmation prompts (accept all defaults)
3033
+ --clean-global-codex Also remove a stale global Codex MCP entry for this project
3034
+ --help Show this help
3029
3035
  `.trim();
3030
3036
  function copyFile(src, dest) {
3031
3037
  const destDir = path9.dirname(dest);
@@ -3045,10 +3051,10 @@ var MCP_SERVER_ENTRY = {
3045
3051
  TYPEGRAPH_TSCONFIG: "./tsconfig.json"
3046
3052
  }
3047
3053
  };
3048
- function getAbsoluteMcpServerEntry(projectRoot3) {
3054
+ function getCodexMcpServerEntry(projectRoot3) {
3049
3055
  return {
3050
- command: "npx",
3051
- args: ["tsx", path9.resolve(projectRoot3, PLUGIN_DIR_NAME, "server.ts")],
3056
+ command: path9.resolve(projectRoot3, PLUGIN_DIR_NAME, "node_modules/.bin/tsx"),
3057
+ args: [path9.resolve(projectRoot3, PLUGIN_DIR_NAME, "server.ts")],
3052
3058
  env: {
3053
3059
  TYPEGRAPH_PROJECT_ROOT: projectRoot3,
3054
3060
  TYPEGRAPH_TSCONFIG: path9.resolve(projectRoot3, "tsconfig.json")
@@ -3058,8 +3064,45 @@ function getAbsoluteMcpServerEntry(projectRoot3) {
3058
3064
  function getCodexConfigPath(projectRoot3) {
3059
3065
  return path9.resolve(projectRoot3, ".codex/config.toml");
3060
3066
  }
3061
- function hasCodexMcpSection(content) {
3062
- return content.includes("[mcp_servers.typegraph]");
3067
+ function isTomlSectionGroup(sectionName, prefix) {
3068
+ return sectionName === prefix || sectionName?.startsWith(`${prefix}.`) === true;
3069
+ }
3070
+ function splitTomlBlocks(content) {
3071
+ const lines = content.split(/\r?\n/);
3072
+ const blocks = [];
3073
+ let sectionName = null;
3074
+ let currentLines = [];
3075
+ for (const line of lines) {
3076
+ const match = line.match(/^\[([^\]]+)\]\s*$/);
3077
+ if (match) {
3078
+ if (currentLines.length > 0 || sectionName !== null) {
3079
+ blocks.push({ sectionName, raw: currentLines.join("\n") });
3080
+ }
3081
+ sectionName = match[1];
3082
+ currentLines = [line];
3083
+ continue;
3084
+ }
3085
+ currentLines.push(line);
3086
+ }
3087
+ if (currentLines.length > 0 || sectionName !== null) {
3088
+ blocks.push({ sectionName, raw: currentLines.join("\n") });
3089
+ }
3090
+ return blocks;
3091
+ }
3092
+ function removeTomlSectionGroup(content, prefix) {
3093
+ const blocks = splitTomlBlocks(content);
3094
+ const removedBlocks = blocks.filter((block) => isTomlSectionGroup(block.sectionName, prefix));
3095
+ if (removedBlocks.length === 0) {
3096
+ return { content, removed: false, removedContent: "" };
3097
+ }
3098
+ const keptBlocks = blocks.filter((block) => !isTomlSectionGroup(block.sectionName, prefix));
3099
+ const nextContent = keptBlocks.map((block) => block.raw).join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
3100
+ return {
3101
+ content: nextContent ? `${nextContent}
3102
+ ` : "",
3103
+ removed: true,
3104
+ removedContent: removedBlocks.map((block) => block.raw).join("\n").trim()
3105
+ };
3063
3106
  }
3064
3107
  function upsertCodexMcpSection(content, block) {
3065
3108
  const sectionRe = /\n?\[mcp_servers\.typegraph\]\n[\s\S]*?(?=\n\[|$)/;
@@ -3078,12 +3121,13 @@ ${normalizedBlock}
3078
3121
  return { content: nextContent, changed: true };
3079
3122
  }
3080
3123
  function makeCodexMcpBlock(projectRoot3) {
3081
- const absoluteEntry = getAbsoluteMcpServerEntry(projectRoot3);
3124
+ const absoluteEntry = getCodexMcpServerEntry(projectRoot3);
3125
+ const args2 = absoluteEntry.args.map((arg) => `"${arg}"`).join(", ");
3082
3126
  return [
3083
3127
  "",
3084
3128
  "[mcp_servers.typegraph]",
3085
3129
  `command = "${absoluteEntry.command}"`,
3086
- `args = ["${absoluteEntry.args[0]}", "${absoluteEntry.args[1]}"]`,
3130
+ `args = [${args2}]`,
3087
3131
  `env = { TYPEGRAPH_PROJECT_ROOT = "${absoluteEntry.env.TYPEGRAPH_PROJECT_ROOT}", TYPEGRAPH_TSCONFIG = "${absoluteEntry.env.TYPEGRAPH_TSCONFIG}" }`,
3088
3132
  ""
3089
3133
  ].join("\n");
@@ -3118,6 +3162,71 @@ function isCodexProjectTrusted(projectRoot3) {
3118
3162
  }
3119
3163
  return matchesTrustedProject();
3120
3164
  }
3165
+ function pathEqualsOrContains(candidatePath, targetPath) {
3166
+ const resolvedCandidate = path9.resolve(candidatePath);
3167
+ const resolvedTarget = path9.resolve(targetPath);
3168
+ if (resolvedCandidate === resolvedTarget || resolvedCandidate.startsWith(`${resolvedTarget}${path9.sep}`)) {
3169
+ return true;
3170
+ }
3171
+ try {
3172
+ const realCandidate = fs8.realpathSync(candidatePath);
3173
+ const realTarget = fs8.realpathSync(targetPath);
3174
+ return realCandidate === realTarget || realCandidate.startsWith(`${realTarget}${path9.sep}`);
3175
+ } catch {
3176
+ return false;
3177
+ }
3178
+ }
3179
+ function findLegacyGlobalCodexCleanup(projectRoot3) {
3180
+ const home = process.env.HOME;
3181
+ if (!home) return null;
3182
+ const globalConfigPath = path9.join(home, ".codex/config.toml");
3183
+ if (!fs8.existsSync(globalConfigPath)) return null;
3184
+ const content = fs8.readFileSync(globalConfigPath, "utf-8");
3185
+ const { content: nextContent, removed, removedContent } = removeTomlSectionGroup(content, "mcp_servers.typegraph");
3186
+ if (!removed) return null;
3187
+ const pluginRoot = path9.resolve(projectRoot3, PLUGIN_DIR_NAME);
3188
+ const quotedPaths = Array.from(removedContent.matchAll(/"([^"\n]+)"/g), (match) => match[1]);
3189
+ const looksProjectSpecific = quotedPaths.some(
3190
+ (quotedPath) => pathEqualsOrContains(quotedPath, projectRoot3) || pathEqualsOrContains(quotedPath, pluginRoot)
3191
+ );
3192
+ if (!looksProjectSpecific) {
3193
+ return null;
3194
+ }
3195
+ return { globalConfigPath, nextContent };
3196
+ }
3197
+ function removeLegacyGlobalCodexMcp(cleanup) {
3198
+ if (cleanup.nextContent === "") {
3199
+ fs8.unlinkSync(cleanup.globalConfigPath);
3200
+ } else {
3201
+ fs8.writeFileSync(cleanup.globalConfigPath, cleanup.nextContent);
3202
+ }
3203
+ p.log.info("~/.codex/config.toml: removed stale global typegraph MCP server entry for this project");
3204
+ }
3205
+ async function resolveRemovePluginOptions(projectRoot3, yes2, cleanGlobalCodex) {
3206
+ const legacyGlobalCodexCleanup = findLegacyGlobalCodexCleanup(projectRoot3);
3207
+ let removeGlobalCodex = cleanGlobalCodex;
3208
+ if (legacyGlobalCodexCleanup && !cleanGlobalCodex && !yes2) {
3209
+ const shouldRemoveGlobal = await p.confirm({
3210
+ message: "Also remove the stale global Codex MCP entry for this project from ~/.codex/config.toml?",
3211
+ initialValue: false
3212
+ });
3213
+ if (p.isCancel(shouldRemoveGlobal)) {
3214
+ p.cancel("Removal cancelled.");
3215
+ process.exit(0);
3216
+ }
3217
+ removeGlobalCodex = shouldRemoveGlobal;
3218
+ }
3219
+ return {
3220
+ removeGlobalCodex,
3221
+ legacyGlobalCodexCleanup,
3222
+ warnAboutGlobalCodex: legacyGlobalCodexCleanup !== null && !removeGlobalCodex
3223
+ };
3224
+ }
3225
+ function warnAboutStaleGlobalCodex() {
3226
+ p.log.warn(
3227
+ "Left a stale global Codex MCP entry for this project in ~/.codex/config.toml. Codex may show MCP startup warnings or errors until you remove it. Re-run `typegraph-mcp remove --clean-global-codex` or remove the `typegraph` block manually."
3228
+ );
3229
+ }
3121
3230
  function registerMcpServers(projectRoot3, selectedAgents) {
3122
3231
  if (selectedAgents.includes("cursor")) {
3123
3232
  registerJsonMcp(projectRoot3, ".cursor/mcp.json", "mcpServers");
@@ -3205,20 +3314,18 @@ function registerCodexMcp(projectRoot3) {
3205
3314
  function deregisterCodexMcp(projectRoot3) {
3206
3315
  const configPath = ".codex/config.toml";
3207
3316
  const fullPath = getCodexConfigPath(projectRoot3);
3208
- if (!fs8.existsSync(fullPath)) return;
3209
- let content = fs8.readFileSync(fullPath, "utf-8");
3210
- if (!hasCodexMcpSection(content)) return;
3211
- content = content.replace(
3212
- /\n?\[mcp_servers\.typegraph\]\n[\s\S]*?(?=\n\[|$)/,
3213
- ""
3214
- );
3215
- content = content.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
3216
- if (content.trim() === "") {
3217
- fs8.unlinkSync(fullPath);
3218
- } else {
3219
- fs8.writeFileSync(fullPath, content);
3317
+ if (fs8.existsSync(fullPath)) {
3318
+ const content = fs8.readFileSync(fullPath, "utf-8");
3319
+ const { content: nextContent, removed } = removeTomlSectionGroup(content, "mcp_servers.typegraph");
3320
+ if (removed) {
3321
+ if (nextContent === "") {
3322
+ fs8.unlinkSync(fullPath);
3323
+ } else {
3324
+ fs8.writeFileSync(fullPath, nextContent);
3325
+ }
3326
+ p.log.info(`${configPath}: removed typegraph MCP server`);
3327
+ }
3220
3328
  }
3221
- p.log.info(`${configPath}: removed typegraph MCP server`);
3222
3329
  }
3223
3330
  function ensureTsconfigExclude(projectRoot3) {
3224
3331
  const tsconfigPath3 = path9.resolve(projectRoot3, "tsconfig.json");
@@ -3357,7 +3464,11 @@ async function setup(yes2) {
3357
3464
  process.exit(0);
3358
3465
  }
3359
3466
  if (action === "remove") {
3360
- await removePlugin(projectRoot3, targetDir);
3467
+ const removeOptions = await resolveRemovePluginOptions(projectRoot3, false, false);
3468
+ await removePlugin(projectRoot3, targetDir, removeOptions);
3469
+ if (removeOptions.warnAboutGlobalCodex) {
3470
+ warnAboutStaleGlobalCodex();
3471
+ }
3361
3472
  return;
3362
3473
  }
3363
3474
  if (action === "exit") {
@@ -3460,9 +3571,13 @@ async function setup(yes2) {
3460
3571
  ensureEslintIgnore(projectRoot3);
3461
3572
  await runVerification(targetDir, selectedAgents);
3462
3573
  }
3463
- async function removePlugin(projectRoot3, pluginDir) {
3574
+ async function removePlugin(projectRoot3, pluginDir, options) {
3464
3575
  const s = p.spinner();
3465
3576
  s.start("Removing typegraph-mcp...");
3577
+ deregisterMcpServers(projectRoot3);
3578
+ if (options.removeGlobalCodex && options.legacyGlobalCodexCleanup) {
3579
+ removeLegacyGlobalCodexMcp(options.legacyGlobalCodexCleanup);
3580
+ }
3466
3581
  if (fs8.existsSync(pluginDir)) {
3467
3582
  fs8.rmSync(pluginDir, { recursive: true });
3468
3583
  }
@@ -3502,7 +3617,6 @@ async function removePlugin(projectRoot3, pluginDir) {
3502
3617
  fs8.writeFileSync(claudeMdPath, content);
3503
3618
  }
3504
3619
  s.stop("Removed typegraph-mcp");
3505
- deregisterMcpServers(projectRoot3);
3506
3620
  p.outro("typegraph-mcp has been uninstalled from this project.");
3507
3621
  }
3508
3622
  async function setupAgentInstructions(projectRoot3, selectedAgents) {
@@ -3582,6 +3696,7 @@ async function runVerification(pluginDir, selectedAgents) {
3582
3696
  async function remove(yes2) {
3583
3697
  const projectRoot3 = process.cwd();
3584
3698
  const pluginDir = path9.resolve(projectRoot3, PLUGIN_DIR_NAME);
3699
+ const cleanGlobalCodex = args.includes("--clean-global-codex");
3585
3700
  process.stdout.write("\x1Bc");
3586
3701
  p.intro("TypeGraph MCP Remove");
3587
3702
  if (!fs8.existsSync(pluginDir)) {
@@ -3595,7 +3710,11 @@ async function remove(yes2) {
3595
3710
  process.exit(0);
3596
3711
  }
3597
3712
  }
3598
- await removePlugin(projectRoot3, pluginDir);
3713
+ const removeOptions = await resolveRemovePluginOptions(projectRoot3, yes2, cleanGlobalCodex);
3714
+ await removePlugin(projectRoot3, pluginDir, removeOptions);
3715
+ if (removeOptions.warnAboutGlobalCodex) {
3716
+ warnAboutStaleGlobalCodex();
3717
+ }
3599
3718
  }
3600
3719
  function resolvePluginDir() {
3601
3720
  const installed = path9.resolve(process.cwd(), PLUGIN_DIR_NAME);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typegraph-mcp",
3
- "version": "0.9.34",
3
+ "version": "0.9.36",
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",