typegraph-mcp 0.9.34 → 0.9.35

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,11 @@ interface AgentDef {
35
36
  detect: (projectRoot: string) => boolean;
36
37
  }
37
38
 
39
+ interface LegacyGlobalCodexCleanup {
40
+ globalConfigPath: string;
41
+ nextContent: string;
42
+ }
43
+
38
44
  // ─── Constants ───────────────────────────────────────────────────────────────
39
45
 
40
46
  const AGENT_SNIPPET = `
@@ -163,8 +169,9 @@ Commands:
163
169
  start Start the MCP server (stdin/stdout)
164
170
 
165
171
  Options:
166
- --yes Skip confirmation prompts (accept all defaults)
167
- --help Show this help
172
+ --yes Skip confirmation prompts (accept all defaults)
173
+ --clean-global-codex Also remove a stale global Codex MCP entry for this project
174
+ --help Show this help
168
175
  `.trim();
169
176
 
170
177
  // ─── Helpers ─────────────────────────────────────────────────────────────────
@@ -207,12 +214,78 @@ function getAbsoluteMcpServerEntry(projectRoot: string): {
207
214
  };
208
215
  }
209
216
 
217
+ function getCodexMcpServerEntry(projectRoot: string): {
218
+ command: string;
219
+ args: string[];
220
+ env: Record<string, string>;
221
+ } {
222
+ return {
223
+ command: path.resolve(projectRoot, PLUGIN_DIR_NAME, "node_modules/.bin/tsx"),
224
+ args: [path.resolve(projectRoot, PLUGIN_DIR_NAME, "server.ts")],
225
+ env: {
226
+ TYPEGRAPH_PROJECT_ROOT: projectRoot,
227
+ TYPEGRAPH_TSCONFIG: path.resolve(projectRoot, "tsconfig.json"),
228
+ },
229
+ };
230
+ }
231
+
210
232
  function getCodexConfigPath(projectRoot: string): string {
211
233
  return path.resolve(projectRoot, ".codex/config.toml");
212
234
  }
213
235
 
214
- function hasCodexMcpSection(content: string): boolean {
215
- return content.includes("[mcp_servers.typegraph]");
236
+ function isTomlSectionGroup(sectionName: string | null, prefix: string): boolean {
237
+ return sectionName === prefix || sectionName?.startsWith(`${prefix}.`) === true;
238
+ }
239
+
240
+ function splitTomlBlocks(content: string): Array<{ sectionName: string | null; raw: string }> {
241
+ const lines = content.split(/\r?\n/);
242
+ const blocks: Array<{ sectionName: string | null; raw: string }> = [];
243
+ let sectionName: string | null = null;
244
+ let currentLines: string[] = [];
245
+
246
+ for (const line of lines) {
247
+ const match = line.match(/^\[([^\]]+)\]\s*$/);
248
+ if (match) {
249
+ if (currentLines.length > 0 || sectionName !== null) {
250
+ blocks.push({ sectionName, raw: currentLines.join("\n") });
251
+ }
252
+ sectionName = match[1]!;
253
+ currentLines = [line];
254
+ continue;
255
+ }
256
+
257
+ currentLines.push(line);
258
+ }
259
+
260
+ if (currentLines.length > 0 || sectionName !== null) {
261
+ blocks.push({ sectionName, raw: currentLines.join("\n") });
262
+ }
263
+
264
+ return blocks;
265
+ }
266
+
267
+ function removeTomlSectionGroup(
268
+ content: string,
269
+ prefix: string
270
+ ): { content: string; removed: boolean; removedContent: string } {
271
+ const blocks = splitTomlBlocks(content);
272
+ const removedBlocks = blocks.filter((block) => isTomlSectionGroup(block.sectionName, prefix));
273
+ if (removedBlocks.length === 0) {
274
+ return { content, removed: false, removedContent: "" };
275
+ }
276
+
277
+ const keptBlocks = blocks.filter((block) => !isTomlSectionGroup(block.sectionName, prefix));
278
+ const nextContent = keptBlocks
279
+ .map((block) => block.raw)
280
+ .join("\n")
281
+ .replace(/\n{3,}/g, "\n\n")
282
+ .trimEnd();
283
+
284
+ return {
285
+ content: nextContent ? `${nextContent}\n` : "",
286
+ removed: true,
287
+ removedContent: removedBlocks.map((block) => block.raw).join("\n").trim(),
288
+ };
216
289
  }
217
290
 
218
291
  function upsertCodexMcpSection(content: string, block: string): { content: string; changed: boolean } {
@@ -236,12 +309,13 @@ function upsertCodexMcpSection(content: string, block: string): { content: strin
236
309
  }
237
310
 
238
311
  function makeCodexMcpBlock(projectRoot: string): string {
239
- const absoluteEntry = getAbsoluteMcpServerEntry(projectRoot);
312
+ const absoluteEntry = getCodexMcpServerEntry(projectRoot);
313
+ const args = absoluteEntry.args.map((arg) => `"${arg}"`).join(", ");
240
314
  return [
241
315
  "",
242
316
  "[mcp_servers.typegraph]",
243
317
  `command = "${absoluteEntry.command}"`,
244
- `args = ["${absoluteEntry.args[0]}", "${absoluteEntry.args[1]}"]`,
318
+ `args = [${args}]`,
245
319
  `env = { TYPEGRAPH_PROJECT_ROOT = "${absoluteEntry.env.TYPEGRAPH_PROJECT_ROOT}", TYPEGRAPH_TSCONFIG = "${absoluteEntry.env.TYPEGRAPH_TSCONFIG}" }`,
246
320
  "",
247
321
  ].join("\n");
@@ -288,6 +362,57 @@ function isCodexProjectTrusted(projectRoot: string): boolean {
288
362
  return matchesTrustedProject();
289
363
  }
290
364
 
365
+ function pathEqualsOrContains(candidatePath: string, targetPath: string): boolean {
366
+ const resolvedCandidate = path.resolve(candidatePath);
367
+ const resolvedTarget = path.resolve(targetPath);
368
+ if (resolvedCandidate === resolvedTarget || resolvedCandidate.startsWith(`${resolvedTarget}${path.sep}`)) {
369
+ return true;
370
+ }
371
+
372
+ try {
373
+ const realCandidate = fs.realpathSync(candidatePath);
374
+ const realTarget = fs.realpathSync(targetPath);
375
+ return realCandidate === realTarget || realCandidate.startsWith(`${realTarget}${path.sep}`);
376
+ } catch {
377
+ return false;
378
+ }
379
+ }
380
+
381
+ function findLegacyGlobalCodexCleanup(projectRoot: string): LegacyGlobalCodexCleanup | null {
382
+ const home = process.env.HOME;
383
+ if (!home) return null;
384
+
385
+ const globalConfigPath = path.join(home, ".codex/config.toml");
386
+ if (!fs.existsSync(globalConfigPath)) return null;
387
+
388
+ const content = fs.readFileSync(globalConfigPath, "utf-8");
389
+ const { content: nextContent, removed, removedContent } = removeTomlSectionGroup(content, "mcp_servers.typegraph");
390
+ if (!removed) return null;
391
+
392
+ const pluginRoot = path.resolve(projectRoot, PLUGIN_DIR_NAME);
393
+ const quotedPaths = Array.from(removedContent.matchAll(/"([^"\n]+)"/g), (match) => match[1]!);
394
+ const looksProjectSpecific = quotedPaths.some((quotedPath) =>
395
+ pathEqualsOrContains(quotedPath, projectRoot) ||
396
+ pathEqualsOrContains(quotedPath, pluginRoot)
397
+ );
398
+
399
+ if (!looksProjectSpecific) {
400
+ return null;
401
+ }
402
+
403
+ return { globalConfigPath, nextContent };
404
+ }
405
+
406
+ function removeLegacyGlobalCodexMcp(cleanup: LegacyGlobalCodexCleanup): void {
407
+ if (cleanup.nextContent === "") {
408
+ fs.unlinkSync(cleanup.globalConfigPath);
409
+ } else {
410
+ fs.writeFileSync(cleanup.globalConfigPath, cleanup.nextContent);
411
+ }
412
+
413
+ p.log.info("~/.codex/config.toml: removed stale global typegraph MCP server entry for this project");
414
+ }
415
+
291
416
  /** Register the typegraph MCP server in agent-specific config files */
292
417
  function registerMcpServers(projectRoot: string, selectedAgents: AgentId[]): void {
293
418
  if (selectedAgents.includes("cursor")) {
@@ -401,26 +526,19 @@ function registerCodexMcp(projectRoot: string): void {
401
526
  function deregisterCodexMcp(projectRoot: string): void {
402
527
  const configPath = ".codex/config.toml";
403
528
  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);
529
+ if (fs.existsSync(fullPath)) {
530
+ const content = fs.readFileSync(fullPath, "utf-8");
531
+ const { content: nextContent, removed } = removeTomlSectionGroup(content, "mcp_servers.typegraph");
532
+
533
+ if (removed) {
534
+ if (nextContent === "") {
535
+ fs.unlinkSync(fullPath);
536
+ } else {
537
+ fs.writeFileSync(fullPath, nextContent);
538
+ }
539
+ p.log.info(`${configPath}: removed typegraph MCP server`);
540
+ }
422
541
  }
423
- p.log.info(`${configPath}: removed typegraph MCP server`);
424
542
  }
425
543
 
426
544
  // ─── TSConfig Exclude ─────────────────────────────────────────────────────────
@@ -748,16 +866,26 @@ async function setup(yes: boolean): Promise<void> {
748
866
 
749
867
  // ─── Remove Command ──────────────────────────────────────────────────────────
750
868
 
751
- async function removePlugin(projectRoot: string, pluginDir: string): Promise<void> {
869
+ async function removePlugin(
870
+ projectRoot: string,
871
+ pluginDir: string,
872
+ options: { removeGlobalCodex: boolean; legacyGlobalCodexCleanup: LegacyGlobalCodexCleanup | null }
873
+ ): Promise<void> {
752
874
  const s = p.spinner();
753
875
  s.start("Removing typegraph-mcp...");
754
876
 
755
- // 1. Remove plugin directory
877
+ // 1. Deregister MCP server from agent config files while project paths still exist
878
+ deregisterMcpServers(projectRoot);
879
+ if (options.removeGlobalCodex && options.legacyGlobalCodexCleanup) {
880
+ removeLegacyGlobalCodexMcp(options.legacyGlobalCodexCleanup);
881
+ }
882
+
883
+ // 2. Remove plugin directory
756
884
  if (fs.existsSync(pluginDir)) {
757
885
  fs.rmSync(pluginDir, { recursive: true });
758
886
  }
759
887
 
760
- // 2. Remove .agents/skills/ entries (only typegraph-mcp skills, not the whole dir)
888
+ // 3. Remove .agents/skills/ entries (only typegraph-mcp skills, not the whole dir)
761
889
  const agentsSkillsDir = path.resolve(projectRoot, ".agents/skills");
762
890
  for (const skill of SKILL_NAMES) {
763
891
  const skillDir = path.join(agentsSkillsDir, skill);
@@ -774,7 +902,7 @@ async function removePlugin(projectRoot: string, pluginDir: string): Promise<voi
774
902
  }
775
903
  }
776
904
 
777
- // 3. Remove agent instruction snippet from all known agent files
905
+ // 4. Remove agent instruction snippet from all known agent files
778
906
  const allAgentFiles = AGENT_IDS
779
907
  .map((id) => AGENTS[id].agentFile)
780
908
  .filter((f): f is string => f !== null);
@@ -797,7 +925,7 @@ async function removePlugin(projectRoot: string, pluginDir: string): Promise<voi
797
925
  }
798
926
  }
799
927
 
800
- // 4. Remove --plugin-dir ./plugins/typegraph-mcp from CLAUDE.md
928
+ // 5. Remove --plugin-dir ./plugins/typegraph-mcp from CLAUDE.md
801
929
  const claudeMdPath = path.resolve(projectRoot, "CLAUDE.md");
802
930
  if (fs.existsSync(claudeMdPath)) {
803
931
  let content = fs.readFileSync(claudeMdPath, "utf-8");
@@ -807,9 +935,6 @@ async function removePlugin(projectRoot: string, pluginDir: string): Promise<voi
807
935
 
808
936
  s.stop("Removed typegraph-mcp");
809
937
 
810
- // 5. Deregister MCP server from agent config files
811
- deregisterMcpServers(projectRoot);
812
-
813
938
  p.outro("typegraph-mcp has been uninstalled from this project.");
814
939
  }
815
940
 
@@ -913,6 +1038,7 @@ async function runVerification(pluginDir: string, selectedAgents: AgentId[]): Pr
913
1038
  async function remove(yes: boolean): Promise<void> {
914
1039
  const projectRoot = process.cwd();
915
1040
  const pluginDir = path.resolve(projectRoot, PLUGIN_DIR_NAME);
1041
+ const cleanGlobalCodex = args.includes("--clean-global-codex");
916
1042
 
917
1043
  process.stdout.write("\x1Bc");
918
1044
  p.intro("TypeGraph MCP Remove");
@@ -930,7 +1056,33 @@ async function remove(yes: boolean): Promise<void> {
930
1056
  }
931
1057
  }
932
1058
 
933
- await removePlugin(projectRoot, pluginDir);
1059
+ const legacyGlobalCodexCleanup = findLegacyGlobalCodexCleanup(projectRoot);
1060
+ let removeGlobalCodex = cleanGlobalCodex;
1061
+
1062
+ if (legacyGlobalCodexCleanup && !cleanGlobalCodex && !yes) {
1063
+ const shouldRemoveGlobal = await p.confirm({
1064
+ message: "Also remove the stale global Codex MCP entry for this project from ~/.codex/config.toml?",
1065
+ initialValue: false,
1066
+ });
1067
+ if (p.isCancel(shouldRemoveGlobal)) {
1068
+ p.cancel("Removal cancelled.");
1069
+ process.exit(0);
1070
+ }
1071
+ removeGlobalCodex = shouldRemoveGlobal;
1072
+ }
1073
+
1074
+ await removePlugin(projectRoot, pluginDir, {
1075
+ removeGlobalCodex,
1076
+ legacyGlobalCodexCleanup,
1077
+ });
1078
+
1079
+ if (legacyGlobalCodexCleanup && !removeGlobalCodex) {
1080
+ p.log.warn(
1081
+ "Left a stale global Codex MCP entry for this project in ~/.codex/config.toml. " +
1082
+ "Codex may show MCP startup warnings or errors until you remove it. " +
1083
+ "Re-run `typegraph-mcp remove --clean-global-codex` or remove the `typegraph` block manually."
1084
+ );
1085
+ }
934
1086
  }
935
1087
 
936
1088
  // ─── 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,46 @@ 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
+ }
3121
3205
  function registerMcpServers(projectRoot3, selectedAgents) {
3122
3206
  if (selectedAgents.includes("cursor")) {
3123
3207
  registerJsonMcp(projectRoot3, ".cursor/mcp.json", "mcpServers");
@@ -3205,20 +3289,18 @@ function registerCodexMcp(projectRoot3) {
3205
3289
  function deregisterCodexMcp(projectRoot3) {
3206
3290
  const configPath = ".codex/config.toml";
3207
3291
  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);
3292
+ if (fs8.existsSync(fullPath)) {
3293
+ const content = fs8.readFileSync(fullPath, "utf-8");
3294
+ const { content: nextContent, removed } = removeTomlSectionGroup(content, "mcp_servers.typegraph");
3295
+ if (removed) {
3296
+ if (nextContent === "") {
3297
+ fs8.unlinkSync(fullPath);
3298
+ } else {
3299
+ fs8.writeFileSync(fullPath, nextContent);
3300
+ }
3301
+ p.log.info(`${configPath}: removed typegraph MCP server`);
3302
+ }
3220
3303
  }
3221
- p.log.info(`${configPath}: removed typegraph MCP server`);
3222
3304
  }
3223
3305
  function ensureTsconfigExclude(projectRoot3) {
3224
3306
  const tsconfigPath3 = path9.resolve(projectRoot3, "tsconfig.json");
@@ -3460,9 +3542,13 @@ async function setup(yes2) {
3460
3542
  ensureEslintIgnore(projectRoot3);
3461
3543
  await runVerification(targetDir, selectedAgents);
3462
3544
  }
3463
- async function removePlugin(projectRoot3, pluginDir) {
3545
+ async function removePlugin(projectRoot3, pluginDir, options) {
3464
3546
  const s = p.spinner();
3465
3547
  s.start("Removing typegraph-mcp...");
3548
+ deregisterMcpServers(projectRoot3);
3549
+ if (options.removeGlobalCodex && options.legacyGlobalCodexCleanup) {
3550
+ removeLegacyGlobalCodexMcp(options.legacyGlobalCodexCleanup);
3551
+ }
3466
3552
  if (fs8.existsSync(pluginDir)) {
3467
3553
  fs8.rmSync(pluginDir, { recursive: true });
3468
3554
  }
@@ -3502,7 +3588,6 @@ async function removePlugin(projectRoot3, pluginDir) {
3502
3588
  fs8.writeFileSync(claudeMdPath, content);
3503
3589
  }
3504
3590
  s.stop("Removed typegraph-mcp");
3505
- deregisterMcpServers(projectRoot3);
3506
3591
  p.outro("typegraph-mcp has been uninstalled from this project.");
3507
3592
  }
3508
3593
  async function setupAgentInstructions(projectRoot3, selectedAgents) {
@@ -3582,6 +3667,7 @@ async function runVerification(pluginDir, selectedAgents) {
3582
3667
  async function remove(yes2) {
3583
3668
  const projectRoot3 = process.cwd();
3584
3669
  const pluginDir = path9.resolve(projectRoot3, PLUGIN_DIR_NAME);
3670
+ const cleanGlobalCodex = args.includes("--clean-global-codex");
3585
3671
  process.stdout.write("\x1Bc");
3586
3672
  p.intro("TypeGraph MCP Remove");
3587
3673
  if (!fs8.existsSync(pluginDir)) {
@@ -3595,7 +3681,28 @@ async function remove(yes2) {
3595
3681
  process.exit(0);
3596
3682
  }
3597
3683
  }
3598
- await removePlugin(projectRoot3, pluginDir);
3684
+ const legacyGlobalCodexCleanup = findLegacyGlobalCodexCleanup(projectRoot3);
3685
+ let removeGlobalCodex = cleanGlobalCodex;
3686
+ if (legacyGlobalCodexCleanup && !cleanGlobalCodex && !yes2) {
3687
+ const shouldRemoveGlobal = await p.confirm({
3688
+ message: "Also remove the stale global Codex MCP entry for this project from ~/.codex/config.toml?",
3689
+ initialValue: false
3690
+ });
3691
+ if (p.isCancel(shouldRemoveGlobal)) {
3692
+ p.cancel("Removal cancelled.");
3693
+ process.exit(0);
3694
+ }
3695
+ removeGlobalCodex = shouldRemoveGlobal;
3696
+ }
3697
+ await removePlugin(projectRoot3, pluginDir, {
3698
+ removeGlobalCodex,
3699
+ legacyGlobalCodexCleanup
3700
+ });
3701
+ if (legacyGlobalCodexCleanup && !removeGlobalCodex) {
3702
+ p.log.warn(
3703
+ "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."
3704
+ );
3705
+ }
3599
3706
  }
3600
3707
  function resolvePluginDir() {
3601
3708
  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.35",
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",