typegraph-mcp 0.9.31 → 0.9.33
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 +17 -15
- package/check.ts +150 -9
- package/cli.ts +100 -82
- package/dist/check.js +117 -9
- package/dist/cli.js +197 -82
- package/package.json +5 -1
- package/scripts/ensure-deps.sh +9 -1
package/README.md
CHANGED
|
@@ -80,13 +80,13 @@ This gives you 14 MCP tools, 5 workflow skills that teach Claude *when* and *how
|
|
|
80
80
|
|
|
81
81
|
**Other agents** (Cursor, Codex CLI, Gemini CLI, GitHub Copilot) — restart your agent session. The MCP server and skills are already configured.
|
|
82
82
|
|
|
83
|
-
For **Codex CLI**, setup now
|
|
83
|
+
For **Codex CLI**, setup now writes a project-local `.codex/config.toml` entry using absolute paths so the tools stay scoped to the project and still work when Codex launches from a subdirectory.
|
|
84
84
|
|
|
85
85
|
First query takes ~2s (tsserver warmup). Subsequent queries: 1-60ms.
|
|
86
86
|
|
|
87
87
|
## Requirements
|
|
88
88
|
|
|
89
|
-
- **Node.js** >=
|
|
89
|
+
- **Node.js** >= 22
|
|
90
90
|
- **TypeScript** >= 5.0 in the target project
|
|
91
91
|
- **npm** for dependency installation
|
|
92
92
|
|
|
@@ -141,30 +141,31 @@ npx typegraph-mcp check
|
|
|
141
141
|
|
|
142
142
|
| Symptom | Fix |
|
|
143
143
|
|---|---|
|
|
144
|
-
| Server won't start | `cd plugins/typegraph-mcp && npm install` |
|
|
145
|
-
| "TypeScript not found" |
|
|
144
|
+
| Server won't start | `cd plugins/typegraph-mcp && npm install --include=optional` |
|
|
145
|
+
| "TypeScript not found" | Run `pnpm install` or `npm install`; if TypeScript is not declared, add it to devDependencies first |
|
|
146
146
|
| Tools return empty results | Check `TYPEGRAPH_TSCONFIG` points to the right tsconfig |
|
|
147
147
|
| Build errors from plugins/ | Add `"plugins/**"` to tsconfig.json `exclude` array |
|
|
148
|
+
| `@esbuild/*` or `@rollup/*` package missing | Reinstall with Node 22: `npm install --include=optional` |
|
|
148
149
|
| "npm warn Unknown project config" | Safe to ignore — caused by pnpm settings in your `.npmrc` that npm doesn't recognize |
|
|
149
150
|
|
|
150
151
|
## Manual MCP configuration
|
|
151
152
|
|
|
152
153
|
### Codex CLI
|
|
153
154
|
|
|
154
|
-
|
|
155
|
+
Add this to your project's `.codex/config.toml`:
|
|
155
156
|
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
157
|
+
```toml
|
|
158
|
+
[mcp_servers.typegraph]
|
|
159
|
+
command = "npx"
|
|
160
|
+
args = ["tsx", "/absolute/path/to/your-project/plugins/typegraph-mcp/server.ts"]
|
|
161
|
+
env = { TYPEGRAPH_PROJECT_ROOT = "/absolute/path/to/your-project", TYPEGRAPH_TSCONFIG = "/absolute/path/to/your-project/tsconfig.json" }
|
|
161
162
|
```
|
|
162
163
|
|
|
163
|
-
|
|
164
|
+
Codex only loads project `.codex/config.toml` files for trusted projects. If needed, add this to `~/.codex/config.toml`:
|
|
164
165
|
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
```toml
|
|
167
|
+
[projects."/absolute/path/to/your-project"]
|
|
168
|
+
trust_level = "trusted"
|
|
168
169
|
```
|
|
169
170
|
|
|
170
171
|
### JSON-based MCP clients
|
|
@@ -213,7 +214,8 @@ Two subsystems start concurrently:
|
|
|
213
214
|
```bash
|
|
214
215
|
git clone https://github.com/guyowen/typegraph-mcp.git
|
|
215
216
|
cd typegraph-mcp
|
|
216
|
-
|
|
217
|
+
nvm use
|
|
218
|
+
npm install --include=optional
|
|
217
219
|
```
|
|
218
220
|
|
|
219
221
|
### Run locally against a project
|
package/check.ts
CHANGED
|
@@ -102,6 +102,105 @@ function testTsserver(projectRoot: string): Promise<boolean> {
|
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
function readProjectCodexConfig(projectRoot: string): string | null {
|
|
106
|
+
const configPath = path.resolve(projectRoot, ".codex/config.toml");
|
|
107
|
+
if (!fs.existsSync(configPath)) return null;
|
|
108
|
+
return fs.readFileSync(configPath, "utf-8");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function hasCodexTypegraphRegistration(content: string): boolean {
|
|
112
|
+
return /\[mcp_servers\.typegraph\]/.test(content);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function hasCompleteCodexTypegraphRegistration(content: string): boolean {
|
|
116
|
+
return (
|
|
117
|
+
hasCodexTypegraphRegistration(content) &&
|
|
118
|
+
/command\s*=\s*"[^"]+"/.test(content) &&
|
|
119
|
+
/args\s*=\s*\[[\s\S]*"tsx"/.test(content) &&
|
|
120
|
+
/TYPEGRAPH_PROJECT_ROOT\s*=/.test(content) &&
|
|
121
|
+
/TYPEGRAPH_TSCONFIG\s*=/.test(content)
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function hasTrustedCodexProject(projectRoot: string): boolean | null {
|
|
126
|
+
const home = process.env.HOME;
|
|
127
|
+
if (!home) return null;
|
|
128
|
+
|
|
129
|
+
const globalConfigPath = path.join(home, ".codex/config.toml");
|
|
130
|
+
if (!fs.existsSync(globalConfigPath)) return null;
|
|
131
|
+
|
|
132
|
+
const lines = fs.readFileSync(globalConfigPath, "utf-8").split(/\r?\n/);
|
|
133
|
+
let currentProject: string | null = null;
|
|
134
|
+
let currentTrusted = false;
|
|
135
|
+
|
|
136
|
+
const matchesTrustedProject = (): boolean =>
|
|
137
|
+
currentProject !== null &&
|
|
138
|
+
currentTrusted &&
|
|
139
|
+
(projectRoot === currentProject || projectRoot.startsWith(currentProject + path.sep));
|
|
140
|
+
|
|
141
|
+
for (const line of lines) {
|
|
142
|
+
const sectionMatch = line.match(/^\[projects\."([^"]+)"\]\s*$/);
|
|
143
|
+
if (sectionMatch) {
|
|
144
|
+
if (matchesTrustedProject()) return true;
|
|
145
|
+
currentProject = path.resolve(sectionMatch[1]!);
|
|
146
|
+
currentTrusted = false;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (line.startsWith("[")) {
|
|
151
|
+
if (matchesTrustedProject()) return true;
|
|
152
|
+
currentProject = null;
|
|
153
|
+
currentTrusted = false;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (currentProject && /\btrust_level\s*=\s*"trusted"/.test(line)) {
|
|
158
|
+
currentTrusted = true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return matchesTrustedProject();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function readProjectPackageJson(projectRoot: string): Record<string, unknown> | null {
|
|
166
|
+
const packageJsonPath = path.resolve(projectRoot, "package.json");
|
|
167
|
+
if (!fs.existsSync(packageJsonPath)) return null;
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
return JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as Record<string, unknown>;
|
|
171
|
+
} catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getProjectInstallCommand(projectRoot: string, packageJson: Record<string, unknown> | null): string {
|
|
177
|
+
const packageManager = typeof packageJson?.["packageManager"] === "string"
|
|
178
|
+
? packageJson["packageManager"]
|
|
179
|
+
: "";
|
|
180
|
+
|
|
181
|
+
if (packageManager.startsWith("pnpm@") || fs.existsSync(path.join(projectRoot, "pnpm-lock.yaml"))) {
|
|
182
|
+
return "pnpm install";
|
|
183
|
+
}
|
|
184
|
+
if (packageManager.startsWith("yarn@") || fs.existsSync(path.join(projectRoot, "yarn.lock"))) {
|
|
185
|
+
return "yarn install";
|
|
186
|
+
}
|
|
187
|
+
return "npm install";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function hasDeclaredDependency(packageJson: Record<string, unknown> | null, packageName: string): boolean {
|
|
191
|
+
const depKeys = [
|
|
192
|
+
"dependencies",
|
|
193
|
+
"devDependencies",
|
|
194
|
+
"peerDependencies",
|
|
195
|
+
"optionalDependencies",
|
|
196
|
+
] as const;
|
|
197
|
+
|
|
198
|
+
return depKeys.some((key) => {
|
|
199
|
+
const deps = packageJson?.[key];
|
|
200
|
+
return typeof deps === "object" && deps !== null && packageName in deps;
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
105
204
|
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
106
205
|
|
|
107
206
|
export async function main(configOverride?: TypegraphConfig): Promise<CheckResult> {
|
|
@@ -111,6 +210,8 @@ export async function main(configOverride?: TypegraphConfig): Promise<CheckResul
|
|
|
111
210
|
let passed = 0;
|
|
112
211
|
let failed = 0;
|
|
113
212
|
let warned = 0;
|
|
213
|
+
const projectPackageJson = readProjectPackageJson(projectRoot);
|
|
214
|
+
const installCommand = getProjectInstallCommand(projectRoot, projectPackageJson);
|
|
114
215
|
|
|
115
216
|
function pass(msg: string): void {
|
|
116
217
|
console.log(` \u2713 ${msg}`);
|
|
@@ -142,10 +243,10 @@ export async function main(configOverride?: TypegraphConfig): Promise<CheckResul
|
|
|
142
243
|
// 1. Node.js version
|
|
143
244
|
const nodeVersion = process.version;
|
|
144
245
|
const nodeMajor = parseInt(nodeVersion.slice(1).split(".")[0]!, 10);
|
|
145
|
-
if (nodeMajor >=
|
|
146
|
-
pass(`Node.js ${nodeVersion} (>=
|
|
246
|
+
if (nodeMajor >= 22) {
|
|
247
|
+
pass(`Node.js ${nodeVersion} (>= 22 required)`);
|
|
147
248
|
} else {
|
|
148
|
-
fail(`Node.js ${nodeVersion} is too old`, "Upgrade Node.js to >=
|
|
249
|
+
fail(`Node.js ${nodeVersion} is too old`, "Upgrade Node.js to >= 22");
|
|
149
250
|
}
|
|
150
251
|
|
|
151
252
|
// 2. tsx availability (if we're running, tsx works — but check it's in the project)
|
|
@@ -168,9 +269,14 @@ export async function main(configOverride?: TypegraphConfig): Promise<CheckResul
|
|
|
168
269
|
tsVersion = tsPkg.version;
|
|
169
270
|
pass(`TypeScript found (v${tsVersion})`);
|
|
170
271
|
} catch {
|
|
272
|
+
const hasDeclaredTs = hasDeclaredDependency(projectPackageJson, "typescript");
|
|
171
273
|
fail(
|
|
172
|
-
|
|
173
|
-
|
|
274
|
+
hasDeclaredTs
|
|
275
|
+
? "TypeScript is declared but not installed in project"
|
|
276
|
+
: "TypeScript not found in project",
|
|
277
|
+
hasDeclaredTs
|
|
278
|
+
? `Run \`${installCommand}\` to install project dependencies`
|
|
279
|
+
: `Add \`typescript\` to devDependencies and run \`${installCommand}\``
|
|
174
280
|
);
|
|
175
281
|
}
|
|
176
282
|
|
|
@@ -186,18 +292,50 @@ export async function main(configOverride?: TypegraphConfig): Promise<CheckResul
|
|
|
186
292
|
// Check for plugin .mcp.json in the tool directory (embedded plugin install)
|
|
187
293
|
const pluginMcpPath = path.join(toolDir, ".mcp.json");
|
|
188
294
|
const hasPluginMcp = fs.existsSync(pluginMcpPath) && fs.existsSync(path.join(toolDir, ".claude-plugin/plugin.json"));
|
|
295
|
+
const projectCodexConfig = readProjectCodexConfig(projectRoot);
|
|
296
|
+
const hasProjectCodexRegistration =
|
|
297
|
+
projectCodexConfig !== null && hasCompleteCodexTypegraphRegistration(projectCodexConfig);
|
|
189
298
|
const codexGet = spawnSync("codex", ["mcp", "get", "typegraph"], {
|
|
190
299
|
stdio: "pipe",
|
|
191
300
|
encoding: "utf-8",
|
|
192
301
|
});
|
|
193
|
-
const
|
|
302
|
+
const hasGlobalCodexRegistration = codexGet.status === 0;
|
|
194
303
|
if (process.env.CLAUDE_PLUGIN_ROOT) {
|
|
195
304
|
pass("MCP registered via plugin (CLAUDE_PLUGIN_ROOT set)");
|
|
196
305
|
} else if (hasPluginMcp) {
|
|
197
306
|
pass("MCP registered via plugin (.mcp.json + .claude-plugin/ present)");
|
|
198
|
-
} else if (
|
|
199
|
-
|
|
307
|
+
} else if (projectCodexConfig !== null) {
|
|
308
|
+
const codexConfigPath = path.resolve(projectRoot, ".codex/config.toml");
|
|
309
|
+
const hasSection = hasCodexTypegraphRegistration(projectCodexConfig);
|
|
310
|
+
const hasCommand = /command\s*=\s*"[^"]+"/.test(projectCodexConfig);
|
|
311
|
+
const hasArgs = /args\s*=\s*\[[\s\S]*"tsx"/.test(projectCodexConfig);
|
|
312
|
+
const hasEnvRoot = /TYPEGRAPH_PROJECT_ROOT\s*=/.test(projectCodexConfig);
|
|
313
|
+
const hasEnvTsconfig = /TYPEGRAPH_TSCONFIG\s*=/.test(projectCodexConfig);
|
|
314
|
+
if (hasProjectCodexRegistration) {
|
|
315
|
+
pass("MCP registered in project .codex/config.toml");
|
|
316
|
+
const trusted = hasTrustedCodexProject(projectRoot);
|
|
317
|
+
if (trusted === false) {
|
|
318
|
+
warn(
|
|
319
|
+
"Project Codex config may be ignored",
|
|
320
|
+
"Add the project (or a parent directory) to ~/.codex/config.toml with trust_level = \"trusted\""
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
const issues: string[] = [];
|
|
325
|
+
if (!hasSection) issues.push("[mcp_servers.typegraph] section is missing");
|
|
326
|
+
if (!hasCommand) issues.push("command is missing");
|
|
327
|
+
if (!hasArgs) issues.push("args should include 'tsx'");
|
|
328
|
+
if (!hasEnvRoot) issues.push("TYPEGRAPH_PROJECT_ROOT is missing");
|
|
329
|
+
if (!hasEnvTsconfig) issues.push("TYPEGRAPH_TSCONFIG is missing");
|
|
330
|
+
fail(
|
|
331
|
+
`Project .codex/config.toml registration incomplete: ${issues.join(", ")}`,
|
|
332
|
+
`Update ${codexConfigPath} with a complete [mcp_servers.typegraph] entry`
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
} else if (hasGlobalCodexRegistration) {
|
|
336
|
+
pass("MCP registered in global Codex CLI config");
|
|
200
337
|
} else {
|
|
338
|
+
const codexConfigPath = path.resolve(projectRoot, ".codex/config.toml");
|
|
201
339
|
const mcpJsonPath = path.resolve(projectRoot, ".claude/mcp.json");
|
|
202
340
|
if (fs.existsSync(mcpJsonPath)) {
|
|
203
341
|
try {
|
|
@@ -244,7 +382,10 @@ export async function main(configOverride?: TypegraphConfig): Promise<CheckResul
|
|
|
244
382
|
);
|
|
245
383
|
}
|
|
246
384
|
} else {
|
|
247
|
-
fail(
|
|
385
|
+
fail(
|
|
386
|
+
"No MCP registration found",
|
|
387
|
+
`Create ${codexConfigPath} with [mcp_servers.typegraph] or create .claude/mcp.json with typegraph server registration`
|
|
388
|
+
);
|
|
248
389
|
}
|
|
249
390
|
}
|
|
250
391
|
|
package/cli.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import * as fs from "node:fs";
|
|
17
17
|
import * as path from "node:path";
|
|
18
|
-
import { execSync
|
|
18
|
+
import { execSync } from "node:child_process";
|
|
19
19
|
import * as p from "@clack/prompts";
|
|
20
20
|
import { resolveConfig } from "./config.js";
|
|
21
21
|
|
|
@@ -192,6 +192,87 @@ function getAbsoluteMcpServerEntry(projectRoot: string): {
|
|
|
192
192
|
};
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
function getCodexConfigPath(projectRoot: string): string {
|
|
196
|
+
return path.resolve(projectRoot, ".codex/config.toml");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function hasCodexMcpSection(content: string): boolean {
|
|
200
|
+
return content.includes("[mcp_servers.typegraph]");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function upsertCodexMcpSection(content: string, block: string): { content: string; changed: boolean } {
|
|
204
|
+
const sectionRe = /\n?\[mcp_servers\.typegraph\]\n[\s\S]*?(?=\n\[|$)/;
|
|
205
|
+
const normalizedBlock = block.trim();
|
|
206
|
+
|
|
207
|
+
if (sectionRe.test(content)) {
|
|
208
|
+
const existingSection = (content.match(sectionRe)?.[0] ?? "").trim();
|
|
209
|
+
if (existingSection === normalizedBlock) {
|
|
210
|
+
return { content, changed: false };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const nextContent = content.replace(sectionRe, `\n${normalizedBlock}\n`);
|
|
214
|
+
return { content: nextContent.trimEnd() + "\n", changed: true };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const nextContent = content
|
|
218
|
+
? content.trimEnd() + "\n\n" + normalizedBlock + "\n"
|
|
219
|
+
: normalizedBlock + "\n";
|
|
220
|
+
return { content: nextContent, changed: true };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function makeCodexMcpBlock(projectRoot: string): string {
|
|
224
|
+
const absoluteEntry = getAbsoluteMcpServerEntry(projectRoot);
|
|
225
|
+
return [
|
|
226
|
+
"",
|
|
227
|
+
"[mcp_servers.typegraph]",
|
|
228
|
+
`command = "${absoluteEntry.command}"`,
|
|
229
|
+
`args = ["${absoluteEntry.args[0]}", "${absoluteEntry.args[1]}"]`,
|
|
230
|
+
`env = { TYPEGRAPH_PROJECT_ROOT = "${absoluteEntry.env.TYPEGRAPH_PROJECT_ROOT}", TYPEGRAPH_TSCONFIG = "${absoluteEntry.env.TYPEGRAPH_TSCONFIG}" }`,
|
|
231
|
+
"",
|
|
232
|
+
].join("\n");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function isCodexProjectTrusted(projectRoot: string): boolean {
|
|
236
|
+
const home = process.env.HOME;
|
|
237
|
+
if (!home) return false;
|
|
238
|
+
|
|
239
|
+
const globalConfigPath = path.join(home, ".codex/config.toml");
|
|
240
|
+
if (!fs.existsSync(globalConfigPath)) return false;
|
|
241
|
+
|
|
242
|
+
const content = fs.readFileSync(globalConfigPath, "utf-8");
|
|
243
|
+
const lines = content.split(/\r?\n/);
|
|
244
|
+
let currentProject: string | null = null;
|
|
245
|
+
let currentTrusted = false;
|
|
246
|
+
|
|
247
|
+
const matchesTrustedProject = (): boolean =>
|
|
248
|
+
currentProject !== null &&
|
|
249
|
+
currentTrusted &&
|
|
250
|
+
(projectRoot === currentProject || projectRoot.startsWith(currentProject + path.sep));
|
|
251
|
+
|
|
252
|
+
for (const line of lines) {
|
|
253
|
+
const sectionMatch = line.match(/^\[projects\."([^"]+)"\]\s*$/);
|
|
254
|
+
if (sectionMatch) {
|
|
255
|
+
if (matchesTrustedProject()) return true;
|
|
256
|
+
currentProject = path.resolve(sectionMatch[1]!);
|
|
257
|
+
currentTrusted = false;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (line.startsWith("[")) {
|
|
262
|
+
if (matchesTrustedProject()) return true;
|
|
263
|
+
currentProject = null;
|
|
264
|
+
currentTrusted = false;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (currentProject && /\btrust_level\s*=\s*"trusted"/.test(line)) {
|
|
269
|
+
currentTrusted = true;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return matchesTrustedProject();
|
|
274
|
+
}
|
|
275
|
+
|
|
195
276
|
/** Register the typegraph MCP server in agent-specific config files */
|
|
196
277
|
function registerMcpServers(projectRoot: string, selectedAgents: AgentId[]): void {
|
|
197
278
|
if (selectedAgents.includes("cursor")) {
|
|
@@ -274,104 +355,41 @@ function deregisterJsonMcp(projectRoot: string, configPath: string, rootKey: str
|
|
|
274
355
|
|
|
275
356
|
/** Register MCP server in Codex CLI's TOML config */
|
|
276
357
|
function registerCodexMcp(projectRoot: string): void {
|
|
277
|
-
const absoluteEntry = getAbsoluteMcpServerEntry(projectRoot);
|
|
278
|
-
|
|
279
|
-
const codexGet = spawnSync("codex", ["mcp", "get", "typegraph"], {
|
|
280
|
-
stdio: "pipe",
|
|
281
|
-
encoding: "utf-8",
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
if (codexGet.status === 0) {
|
|
285
|
-
const output = `${codexGet.stdout ?? ""}${codexGet.stderr ?? ""}`;
|
|
286
|
-
const hasServerPath = output.includes(absoluteEntry.args[1]!);
|
|
287
|
-
const hasProjectRoot = output.includes("TYPEGRAPH_PROJECT_ROOT=*****") || output.includes(projectRoot);
|
|
288
|
-
const hasTsconfig = output.includes("TYPEGRAPH_TSCONFIG=*****") || output.includes(path.resolve(projectRoot, "tsconfig.json"));
|
|
289
|
-
if (hasServerPath && hasProjectRoot && hasTsconfig) {
|
|
290
|
-
p.log.info("Codex CLI: typegraph MCP server already registered");
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
spawnSync("codex", ["mcp", "remove", "typegraph"], {
|
|
294
|
-
stdio: "pipe",
|
|
295
|
-
encoding: "utf-8",
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const codexAdd = spawnSync(
|
|
300
|
-
"codex",
|
|
301
|
-
[
|
|
302
|
-
"mcp",
|
|
303
|
-
"add",
|
|
304
|
-
"typegraph",
|
|
305
|
-
"--env",
|
|
306
|
-
`TYPEGRAPH_PROJECT_ROOT=${absoluteEntry.env.TYPEGRAPH_PROJECT_ROOT}`,
|
|
307
|
-
"--env",
|
|
308
|
-
`TYPEGRAPH_TSCONFIG=${absoluteEntry.env.TYPEGRAPH_TSCONFIG}`,
|
|
309
|
-
"--",
|
|
310
|
-
absoluteEntry.command,
|
|
311
|
-
...absoluteEntry.args,
|
|
312
|
-
],
|
|
313
|
-
{
|
|
314
|
-
stdio: "pipe",
|
|
315
|
-
encoding: "utf-8",
|
|
316
|
-
}
|
|
317
|
-
);
|
|
318
|
-
|
|
319
|
-
if (codexAdd.status === 0) {
|
|
320
|
-
p.log.success("Codex CLI: registered typegraph MCP server");
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
p.log.warn(
|
|
325
|
-
`Codex CLI registration failed — falling back to ${".codex/config.toml"}`
|
|
326
|
-
);
|
|
327
|
-
|
|
328
358
|
const configPath = ".codex/config.toml";
|
|
329
|
-
const fullPath =
|
|
359
|
+
const fullPath = getCodexConfigPath(projectRoot);
|
|
360
|
+
const block = makeCodexMcpBlock(projectRoot);
|
|
330
361
|
let content = "";
|
|
331
362
|
|
|
332
363
|
if (fs.existsSync(fullPath)) {
|
|
333
364
|
content = fs.readFileSync(fullPath, "utf-8");
|
|
334
|
-
// Already registered?
|
|
335
|
-
if (content.includes("[mcp_servers.typegraph]")) {
|
|
336
|
-
p.log.info(`${configPath}: typegraph MCP server already registered`);
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
365
|
}
|
|
340
366
|
|
|
341
|
-
const block = [
|
|
342
|
-
"",
|
|
343
|
-
"[mcp_servers.typegraph]",
|
|
344
|
-
`command = "${absoluteEntry.command}"`,
|
|
345
|
-
`args = ["${absoluteEntry.args[0]}", "${absoluteEntry.args[1]}"]`,
|
|
346
|
-
`env = { TYPEGRAPH_PROJECT_ROOT = "${absoluteEntry.env.TYPEGRAPH_PROJECT_ROOT}", TYPEGRAPH_TSCONFIG = "${absoluteEntry.env.TYPEGRAPH_TSCONFIG}" }`,
|
|
347
|
-
"",
|
|
348
|
-
].join("\n");
|
|
349
|
-
|
|
350
367
|
const dir = path.dirname(fullPath);
|
|
351
368
|
if (!fs.existsSync(dir)) {
|
|
352
369
|
fs.mkdirSync(dir, { recursive: true });
|
|
353
370
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
371
|
+
|
|
372
|
+
const { content: nextContent, changed } = upsertCodexMcpSection(content, block);
|
|
373
|
+
if (changed) {
|
|
374
|
+
fs.writeFileSync(fullPath, nextContent);
|
|
375
|
+
p.log.success(`${configPath}: registered typegraph MCP server`);
|
|
376
|
+
} else {
|
|
377
|
+
p.log.info(`${configPath}: typegraph MCP server already registered`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (!isCodexProjectTrusted(projectRoot)) {
|
|
381
|
+
p.log.info(`Codex CLI: trust ${projectRoot} in ~/.codex/config.toml to load project MCP settings`);
|
|
382
|
+
}
|
|
357
383
|
}
|
|
358
384
|
|
|
359
385
|
/** Deregister MCP server from Codex CLI's TOML config */
|
|
360
386
|
function deregisterCodexMcp(projectRoot: string): void {
|
|
361
|
-
const codexRemove = spawnSync("codex", ["mcp", "remove", "typegraph"], {
|
|
362
|
-
stdio: "pipe",
|
|
363
|
-
encoding: "utf-8",
|
|
364
|
-
});
|
|
365
|
-
if (codexRemove.status === 0) {
|
|
366
|
-
p.log.info("Codex CLI: removed typegraph MCP server");
|
|
367
|
-
}
|
|
368
|
-
|
|
369
387
|
const configPath = ".codex/config.toml";
|
|
370
|
-
const fullPath =
|
|
388
|
+
const fullPath = getCodexConfigPath(projectRoot);
|
|
371
389
|
if (!fs.existsSync(fullPath)) return;
|
|
372
390
|
|
|
373
391
|
let content = fs.readFileSync(fullPath, "utf-8");
|
|
374
|
-
if (!content
|
|
392
|
+
if (!hasCodexMcpSection(content)) return;
|
|
375
393
|
|
|
376
394
|
// Remove the [mcp_servers.typegraph] section (stops at next section header or end of file)
|
|
377
395
|
content = content.replace(
|
|
@@ -643,12 +661,12 @@ async function setup(yes: boolean): Promise<void> {
|
|
|
643
661
|
|
|
644
662
|
s.message("Installing dependencies...");
|
|
645
663
|
try {
|
|
646
|
-
execSync("npm install", { cwd: targetDir, stdio: "pipe" });
|
|
664
|
+
execSync("npm install --include=optional", { cwd: targetDir, stdio: "pipe" });
|
|
647
665
|
s.stop(`${isUpdate ? "Updated" : "Installed"} ${copied} files with dependencies`);
|
|
648
666
|
} catch (err) {
|
|
649
667
|
s.stop(`${isUpdate ? "Updated" : "Installed"} ${copied} files`);
|
|
650
668
|
p.log.warn(`Dependency install failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
651
|
-
p.log.info(`Run manually: cd ${PLUGIN_DIR_NAME} && npm install`);
|
|
669
|
+
p.log.info(`Run manually: cd ${PLUGIN_DIR_NAME} && npm install --include=optional`);
|
|
652
670
|
}
|
|
653
671
|
|
|
654
672
|
// 4. Copy skills to .agents/skills/ for cross-platform discovery
|
package/dist/check.js
CHANGED
|
@@ -432,11 +432,84 @@ function testTsserver(projectRoot) {
|
|
|
432
432
|
child.stdin.write(request + "\n");
|
|
433
433
|
});
|
|
434
434
|
}
|
|
435
|
+
function readProjectCodexConfig(projectRoot) {
|
|
436
|
+
const configPath = path3.resolve(projectRoot, ".codex/config.toml");
|
|
437
|
+
if (!fs2.existsSync(configPath)) return null;
|
|
438
|
+
return fs2.readFileSync(configPath, "utf-8");
|
|
439
|
+
}
|
|
440
|
+
function hasCodexTypegraphRegistration(content) {
|
|
441
|
+
return /\[mcp_servers\.typegraph\]/.test(content);
|
|
442
|
+
}
|
|
443
|
+
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);
|
|
445
|
+
}
|
|
446
|
+
function hasTrustedCodexProject(projectRoot) {
|
|
447
|
+
const home = process.env.HOME;
|
|
448
|
+
if (!home) return null;
|
|
449
|
+
const globalConfigPath = path3.join(home, ".codex/config.toml");
|
|
450
|
+
if (!fs2.existsSync(globalConfigPath)) return null;
|
|
451
|
+
const lines = fs2.readFileSync(globalConfigPath, "utf-8").split(/\r?\n/);
|
|
452
|
+
let currentProject = null;
|
|
453
|
+
let currentTrusted = false;
|
|
454
|
+
const matchesTrustedProject = () => currentProject !== null && currentTrusted && (projectRoot === currentProject || projectRoot.startsWith(currentProject + path3.sep));
|
|
455
|
+
for (const line of lines) {
|
|
456
|
+
const sectionMatch = line.match(/^\[projects\."([^"]+)"\]\s*$/);
|
|
457
|
+
if (sectionMatch) {
|
|
458
|
+
if (matchesTrustedProject()) return true;
|
|
459
|
+
currentProject = path3.resolve(sectionMatch[1]);
|
|
460
|
+
currentTrusted = false;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (line.startsWith("[")) {
|
|
464
|
+
if (matchesTrustedProject()) return true;
|
|
465
|
+
currentProject = null;
|
|
466
|
+
currentTrusted = false;
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (currentProject && /\btrust_level\s*=\s*"trusted"/.test(line)) {
|
|
470
|
+
currentTrusted = true;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return matchesTrustedProject();
|
|
474
|
+
}
|
|
475
|
+
function readProjectPackageJson(projectRoot) {
|
|
476
|
+
const packageJsonPath = path3.resolve(projectRoot, "package.json");
|
|
477
|
+
if (!fs2.existsSync(packageJsonPath)) return null;
|
|
478
|
+
try {
|
|
479
|
+
return JSON.parse(fs2.readFileSync(packageJsonPath, "utf-8"));
|
|
480
|
+
} catch {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function getProjectInstallCommand(projectRoot, packageJson) {
|
|
485
|
+
const packageManager = typeof packageJson?.["packageManager"] === "string" ? packageJson["packageManager"] : "";
|
|
486
|
+
if (packageManager.startsWith("pnpm@") || fs2.existsSync(path3.join(projectRoot, "pnpm-lock.yaml"))) {
|
|
487
|
+
return "pnpm install";
|
|
488
|
+
}
|
|
489
|
+
if (packageManager.startsWith("yarn@") || fs2.existsSync(path3.join(projectRoot, "yarn.lock"))) {
|
|
490
|
+
return "yarn install";
|
|
491
|
+
}
|
|
492
|
+
return "npm install";
|
|
493
|
+
}
|
|
494
|
+
function hasDeclaredDependency(packageJson, packageName) {
|
|
495
|
+
const depKeys = [
|
|
496
|
+
"dependencies",
|
|
497
|
+
"devDependencies",
|
|
498
|
+
"peerDependencies",
|
|
499
|
+
"optionalDependencies"
|
|
500
|
+
];
|
|
501
|
+
return depKeys.some((key) => {
|
|
502
|
+
const deps = packageJson?.[key];
|
|
503
|
+
return typeof deps === "object" && deps !== null && packageName in deps;
|
|
504
|
+
});
|
|
505
|
+
}
|
|
435
506
|
async function main(configOverride) {
|
|
436
507
|
const { projectRoot, tsconfigPath, toolDir, toolIsEmbedded, toolRelPath } = configOverride ?? resolveConfig(import.meta.dirname);
|
|
437
508
|
let passed = 0;
|
|
438
509
|
let failed = 0;
|
|
439
510
|
let warned = 0;
|
|
511
|
+
const projectPackageJson = readProjectPackageJson(projectRoot);
|
|
512
|
+
const installCommand = getProjectInstallCommand(projectRoot, projectPackageJson);
|
|
440
513
|
function pass(msg) {
|
|
441
514
|
console.log(` \u2713 ${msg}`);
|
|
442
515
|
passed++;
|
|
@@ -461,10 +534,10 @@ async function main(configOverride) {
|
|
|
461
534
|
console.log("");
|
|
462
535
|
const nodeVersion = process.version;
|
|
463
536
|
const nodeMajor = parseInt(nodeVersion.slice(1).split(".")[0], 10);
|
|
464
|
-
if (nodeMajor >=
|
|
465
|
-
pass(`Node.js ${nodeVersion} (>=
|
|
537
|
+
if (nodeMajor >= 22) {
|
|
538
|
+
pass(`Node.js ${nodeVersion} (>= 22 required)`);
|
|
466
539
|
} else {
|
|
467
|
-
fail(`Node.js ${nodeVersion} is too old`, "Upgrade Node.js to >=
|
|
540
|
+
fail(`Node.js ${nodeVersion} is too old`, "Upgrade Node.js to >= 22");
|
|
468
541
|
}
|
|
469
542
|
const tsxInRoot = fs2.existsSync(path3.join(projectRoot, "node_modules/.bin/tsx"));
|
|
470
543
|
const tsxInTool = fs2.existsSync(path3.join(toolDir, "node_modules/.bin/tsx"));
|
|
@@ -482,9 +555,10 @@ async function main(configOverride) {
|
|
|
482
555
|
tsVersion = tsPkg.version;
|
|
483
556
|
pass(`TypeScript found (v${tsVersion})`);
|
|
484
557
|
} catch {
|
|
558
|
+
const hasDeclaredTs = hasDeclaredDependency(projectPackageJson, "typescript");
|
|
485
559
|
fail(
|
|
486
|
-
"TypeScript not found in project",
|
|
487
|
-
|
|
560
|
+
hasDeclaredTs ? "TypeScript is declared but not installed in project" : "TypeScript not found in project",
|
|
561
|
+
hasDeclaredTs ? `Run \`${installCommand}\` to install project dependencies` : `Add \`typescript\` to devDependencies and run \`${installCommand}\``
|
|
488
562
|
);
|
|
489
563
|
}
|
|
490
564
|
const tsconfigAbs = path3.resolve(projectRoot, tsconfigPath);
|
|
@@ -495,18 +569,49 @@ async function main(configOverride) {
|
|
|
495
569
|
}
|
|
496
570
|
const pluginMcpPath = path3.join(toolDir, ".mcp.json");
|
|
497
571
|
const hasPluginMcp = fs2.existsSync(pluginMcpPath) && fs2.existsSync(path3.join(toolDir, ".claude-plugin/plugin.json"));
|
|
572
|
+
const projectCodexConfig = readProjectCodexConfig(projectRoot);
|
|
573
|
+
const hasProjectCodexRegistration = projectCodexConfig !== null && hasCompleteCodexTypegraphRegistration(projectCodexConfig);
|
|
498
574
|
const codexGet = spawnSync("codex", ["mcp", "get", "typegraph"], {
|
|
499
575
|
stdio: "pipe",
|
|
500
576
|
encoding: "utf-8"
|
|
501
577
|
});
|
|
502
|
-
const
|
|
578
|
+
const hasGlobalCodexRegistration = codexGet.status === 0;
|
|
503
579
|
if (process.env.CLAUDE_PLUGIN_ROOT) {
|
|
504
580
|
pass("MCP registered via plugin (CLAUDE_PLUGIN_ROOT set)");
|
|
505
581
|
} else if (hasPluginMcp) {
|
|
506
582
|
pass("MCP registered via plugin (.mcp.json + .claude-plugin/ present)");
|
|
507
|
-
} else if (
|
|
508
|
-
|
|
583
|
+
} else if (projectCodexConfig !== null) {
|
|
584
|
+
const codexConfigPath = path3.resolve(projectRoot, ".codex/config.toml");
|
|
585
|
+
const hasSection = hasCodexTypegraphRegistration(projectCodexConfig);
|
|
586
|
+
const hasCommand = /command\s*=\s*"[^"]+"/.test(projectCodexConfig);
|
|
587
|
+
const hasArgs = /args\s*=\s*\[[\s\S]*"tsx"/.test(projectCodexConfig);
|
|
588
|
+
const hasEnvRoot = /TYPEGRAPH_PROJECT_ROOT\s*=/.test(projectCodexConfig);
|
|
589
|
+
const hasEnvTsconfig = /TYPEGRAPH_TSCONFIG\s*=/.test(projectCodexConfig);
|
|
590
|
+
if (hasProjectCodexRegistration) {
|
|
591
|
+
pass("MCP registered in project .codex/config.toml");
|
|
592
|
+
const trusted = hasTrustedCodexProject(projectRoot);
|
|
593
|
+
if (trusted === false) {
|
|
594
|
+
warn(
|
|
595
|
+
"Project Codex config may be ignored",
|
|
596
|
+
'Add the project (or a parent directory) to ~/.codex/config.toml with trust_level = "trusted"'
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
} else {
|
|
600
|
+
const issues = [];
|
|
601
|
+
if (!hasSection) issues.push("[mcp_servers.typegraph] section is missing");
|
|
602
|
+
if (!hasCommand) issues.push("command is missing");
|
|
603
|
+
if (!hasArgs) issues.push("args should include 'tsx'");
|
|
604
|
+
if (!hasEnvRoot) issues.push("TYPEGRAPH_PROJECT_ROOT is missing");
|
|
605
|
+
if (!hasEnvTsconfig) issues.push("TYPEGRAPH_TSCONFIG is missing");
|
|
606
|
+
fail(
|
|
607
|
+
`Project .codex/config.toml registration incomplete: ${issues.join(", ")}`,
|
|
608
|
+
`Update ${codexConfigPath} with a complete [mcp_servers.typegraph] entry`
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
} else if (hasGlobalCodexRegistration) {
|
|
612
|
+
pass("MCP registered in global Codex CLI config");
|
|
509
613
|
} else {
|
|
614
|
+
const codexConfigPath = path3.resolve(projectRoot, ".codex/config.toml");
|
|
510
615
|
const mcpJsonPath = path3.resolve(projectRoot, ".claude/mcp.json");
|
|
511
616
|
if (fs2.existsSync(mcpJsonPath)) {
|
|
512
617
|
try {
|
|
@@ -551,7 +656,10 @@ async function main(configOverride) {
|
|
|
551
656
|
);
|
|
552
657
|
}
|
|
553
658
|
} else {
|
|
554
|
-
fail(
|
|
659
|
+
fail(
|
|
660
|
+
"No MCP registration found",
|
|
661
|
+
`Create ${codexConfigPath} with [mcp_servers.typegraph] or create .claude/mcp.json with typegraph server registration`
|
|
662
|
+
);
|
|
555
663
|
}
|
|
556
664
|
}
|
|
557
665
|
const toolNodeModules = path3.join(toolDir, "node_modules");
|
package/dist/cli.js
CHANGED
|
@@ -439,11 +439,84 @@ function testTsserver(projectRoot3) {
|
|
|
439
439
|
child.stdin.write(request + "\n");
|
|
440
440
|
});
|
|
441
441
|
}
|
|
442
|
+
function readProjectCodexConfig(projectRoot3) {
|
|
443
|
+
const configPath = path3.resolve(projectRoot3, ".codex/config.toml");
|
|
444
|
+
if (!fs2.existsSync(configPath)) return null;
|
|
445
|
+
return fs2.readFileSync(configPath, "utf-8");
|
|
446
|
+
}
|
|
447
|
+
function hasCodexTypegraphRegistration(content) {
|
|
448
|
+
return /\[mcp_servers\.typegraph\]/.test(content);
|
|
449
|
+
}
|
|
450
|
+
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);
|
|
452
|
+
}
|
|
453
|
+
function hasTrustedCodexProject(projectRoot3) {
|
|
454
|
+
const home = process.env.HOME;
|
|
455
|
+
if (!home) return null;
|
|
456
|
+
const globalConfigPath = path3.join(home, ".codex/config.toml");
|
|
457
|
+
if (!fs2.existsSync(globalConfigPath)) return null;
|
|
458
|
+
const lines = fs2.readFileSync(globalConfigPath, "utf-8").split(/\r?\n/);
|
|
459
|
+
let currentProject = null;
|
|
460
|
+
let currentTrusted = false;
|
|
461
|
+
const matchesTrustedProject = () => currentProject !== null && currentTrusted && (projectRoot3 === currentProject || projectRoot3.startsWith(currentProject + path3.sep));
|
|
462
|
+
for (const line of lines) {
|
|
463
|
+
const sectionMatch = line.match(/^\[projects\."([^"]+)"\]\s*$/);
|
|
464
|
+
if (sectionMatch) {
|
|
465
|
+
if (matchesTrustedProject()) return true;
|
|
466
|
+
currentProject = path3.resolve(sectionMatch[1]);
|
|
467
|
+
currentTrusted = false;
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (line.startsWith("[")) {
|
|
471
|
+
if (matchesTrustedProject()) return true;
|
|
472
|
+
currentProject = null;
|
|
473
|
+
currentTrusted = false;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
if (currentProject && /\btrust_level\s*=\s*"trusted"/.test(line)) {
|
|
477
|
+
currentTrusted = true;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return matchesTrustedProject();
|
|
481
|
+
}
|
|
482
|
+
function readProjectPackageJson(projectRoot3) {
|
|
483
|
+
const packageJsonPath = path3.resolve(projectRoot3, "package.json");
|
|
484
|
+
if (!fs2.existsSync(packageJsonPath)) return null;
|
|
485
|
+
try {
|
|
486
|
+
return JSON.parse(fs2.readFileSync(packageJsonPath, "utf-8"));
|
|
487
|
+
} catch {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function getProjectInstallCommand(projectRoot3, packageJson) {
|
|
492
|
+
const packageManager = typeof packageJson?.["packageManager"] === "string" ? packageJson["packageManager"] : "";
|
|
493
|
+
if (packageManager.startsWith("pnpm@") || fs2.existsSync(path3.join(projectRoot3, "pnpm-lock.yaml"))) {
|
|
494
|
+
return "pnpm install";
|
|
495
|
+
}
|
|
496
|
+
if (packageManager.startsWith("yarn@") || fs2.existsSync(path3.join(projectRoot3, "yarn.lock"))) {
|
|
497
|
+
return "yarn install";
|
|
498
|
+
}
|
|
499
|
+
return "npm install";
|
|
500
|
+
}
|
|
501
|
+
function hasDeclaredDependency(packageJson, packageName) {
|
|
502
|
+
const depKeys = [
|
|
503
|
+
"dependencies",
|
|
504
|
+
"devDependencies",
|
|
505
|
+
"peerDependencies",
|
|
506
|
+
"optionalDependencies"
|
|
507
|
+
];
|
|
508
|
+
return depKeys.some((key) => {
|
|
509
|
+
const deps = packageJson?.[key];
|
|
510
|
+
return typeof deps === "object" && deps !== null && packageName in deps;
|
|
511
|
+
});
|
|
512
|
+
}
|
|
442
513
|
async function main(configOverride) {
|
|
443
514
|
const { projectRoot: projectRoot3, tsconfigPath: tsconfigPath3, toolDir, toolIsEmbedded, toolRelPath } = configOverride ?? resolveConfig(import.meta.dirname);
|
|
444
515
|
let passed = 0;
|
|
445
516
|
let failed = 0;
|
|
446
517
|
let warned = 0;
|
|
518
|
+
const projectPackageJson = readProjectPackageJson(projectRoot3);
|
|
519
|
+
const installCommand = getProjectInstallCommand(projectRoot3, projectPackageJson);
|
|
447
520
|
function pass(msg) {
|
|
448
521
|
console.log(` \u2713 ${msg}`);
|
|
449
522
|
passed++;
|
|
@@ -468,10 +541,10 @@ async function main(configOverride) {
|
|
|
468
541
|
console.log("");
|
|
469
542
|
const nodeVersion = process.version;
|
|
470
543
|
const nodeMajor = parseInt(nodeVersion.slice(1).split(".")[0], 10);
|
|
471
|
-
if (nodeMajor >=
|
|
472
|
-
pass(`Node.js ${nodeVersion} (>=
|
|
544
|
+
if (nodeMajor >= 22) {
|
|
545
|
+
pass(`Node.js ${nodeVersion} (>= 22 required)`);
|
|
473
546
|
} else {
|
|
474
|
-
fail(`Node.js ${nodeVersion} is too old`, "Upgrade Node.js to >=
|
|
547
|
+
fail(`Node.js ${nodeVersion} is too old`, "Upgrade Node.js to >= 22");
|
|
475
548
|
}
|
|
476
549
|
const tsxInRoot = fs2.existsSync(path3.join(projectRoot3, "node_modules/.bin/tsx"));
|
|
477
550
|
const tsxInTool = fs2.existsSync(path3.join(toolDir, "node_modules/.bin/tsx"));
|
|
@@ -489,9 +562,10 @@ async function main(configOverride) {
|
|
|
489
562
|
tsVersion = tsPkg.version;
|
|
490
563
|
pass(`TypeScript found (v${tsVersion})`);
|
|
491
564
|
} catch {
|
|
565
|
+
const hasDeclaredTs = hasDeclaredDependency(projectPackageJson, "typescript");
|
|
492
566
|
fail(
|
|
493
|
-
"TypeScript not found in project",
|
|
494
|
-
|
|
567
|
+
hasDeclaredTs ? "TypeScript is declared but not installed in project" : "TypeScript not found in project",
|
|
568
|
+
hasDeclaredTs ? `Run \`${installCommand}\` to install project dependencies` : `Add \`typescript\` to devDependencies and run \`${installCommand}\``
|
|
495
569
|
);
|
|
496
570
|
}
|
|
497
571
|
const tsconfigAbs = path3.resolve(projectRoot3, tsconfigPath3);
|
|
@@ -502,18 +576,49 @@ async function main(configOverride) {
|
|
|
502
576
|
}
|
|
503
577
|
const pluginMcpPath = path3.join(toolDir, ".mcp.json");
|
|
504
578
|
const hasPluginMcp = fs2.existsSync(pluginMcpPath) && fs2.existsSync(path3.join(toolDir, ".claude-plugin/plugin.json"));
|
|
579
|
+
const projectCodexConfig = readProjectCodexConfig(projectRoot3);
|
|
580
|
+
const hasProjectCodexRegistration = projectCodexConfig !== null && hasCompleteCodexTypegraphRegistration(projectCodexConfig);
|
|
505
581
|
const codexGet = spawnSync("codex", ["mcp", "get", "typegraph"], {
|
|
506
582
|
stdio: "pipe",
|
|
507
583
|
encoding: "utf-8"
|
|
508
584
|
});
|
|
509
|
-
const
|
|
585
|
+
const hasGlobalCodexRegistration = codexGet.status === 0;
|
|
510
586
|
if (process.env.CLAUDE_PLUGIN_ROOT) {
|
|
511
587
|
pass("MCP registered via plugin (CLAUDE_PLUGIN_ROOT set)");
|
|
512
588
|
} else if (hasPluginMcp) {
|
|
513
589
|
pass("MCP registered via plugin (.mcp.json + .claude-plugin/ present)");
|
|
514
|
-
} else if (
|
|
515
|
-
|
|
590
|
+
} else if (projectCodexConfig !== null) {
|
|
591
|
+
const codexConfigPath = path3.resolve(projectRoot3, ".codex/config.toml");
|
|
592
|
+
const hasSection = hasCodexTypegraphRegistration(projectCodexConfig);
|
|
593
|
+
const hasCommand = /command\s*=\s*"[^"]+"/.test(projectCodexConfig);
|
|
594
|
+
const hasArgs = /args\s*=\s*\[[\s\S]*"tsx"/.test(projectCodexConfig);
|
|
595
|
+
const hasEnvRoot = /TYPEGRAPH_PROJECT_ROOT\s*=/.test(projectCodexConfig);
|
|
596
|
+
const hasEnvTsconfig = /TYPEGRAPH_TSCONFIG\s*=/.test(projectCodexConfig);
|
|
597
|
+
if (hasProjectCodexRegistration) {
|
|
598
|
+
pass("MCP registered in project .codex/config.toml");
|
|
599
|
+
const trusted = hasTrustedCodexProject(projectRoot3);
|
|
600
|
+
if (trusted === false) {
|
|
601
|
+
warn(
|
|
602
|
+
"Project Codex config may be ignored",
|
|
603
|
+
'Add the project (or a parent directory) to ~/.codex/config.toml with trust_level = "trusted"'
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
} else {
|
|
607
|
+
const issues = [];
|
|
608
|
+
if (!hasSection) issues.push("[mcp_servers.typegraph] section is missing");
|
|
609
|
+
if (!hasCommand) issues.push("command is missing");
|
|
610
|
+
if (!hasArgs) issues.push("args should include 'tsx'");
|
|
611
|
+
if (!hasEnvRoot) issues.push("TYPEGRAPH_PROJECT_ROOT is missing");
|
|
612
|
+
if (!hasEnvTsconfig) issues.push("TYPEGRAPH_TSCONFIG is missing");
|
|
613
|
+
fail(
|
|
614
|
+
`Project .codex/config.toml registration incomplete: ${issues.join(", ")}`,
|
|
615
|
+
`Update ${codexConfigPath} with a complete [mcp_servers.typegraph] entry`
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
} else if (hasGlobalCodexRegistration) {
|
|
619
|
+
pass("MCP registered in global Codex CLI config");
|
|
516
620
|
} else {
|
|
621
|
+
const codexConfigPath = path3.resolve(projectRoot3, ".codex/config.toml");
|
|
517
622
|
const mcpJsonPath = path3.resolve(projectRoot3, ".claude/mcp.json");
|
|
518
623
|
if (fs2.existsSync(mcpJsonPath)) {
|
|
519
624
|
try {
|
|
@@ -558,7 +663,10 @@ async function main(configOverride) {
|
|
|
558
663
|
);
|
|
559
664
|
}
|
|
560
665
|
} else {
|
|
561
|
-
fail(
|
|
666
|
+
fail(
|
|
667
|
+
"No MCP registration found",
|
|
668
|
+
`Create ${codexConfigPath} with [mcp_servers.typegraph] or create .claude/mcp.json with typegraph server registration`
|
|
669
|
+
);
|
|
562
670
|
}
|
|
563
671
|
}
|
|
564
672
|
const toolNodeModules = path3.join(toolDir, "node_modules");
|
|
@@ -2802,7 +2910,7 @@ var init_server = __esm({
|
|
|
2802
2910
|
init_config();
|
|
2803
2911
|
import * as fs8 from "fs";
|
|
2804
2912
|
import * as path9 from "path";
|
|
2805
|
-
import { execSync as execSync2
|
|
2913
|
+
import { execSync as execSync2 } from "child_process";
|
|
2806
2914
|
import * as p from "@clack/prompts";
|
|
2807
2915
|
var AGENT_SNIPPET = `
|
|
2808
2916
|
## TypeScript Navigation (typegraph-mcp)
|
|
@@ -2932,6 +3040,69 @@ function getAbsoluteMcpServerEntry(projectRoot3) {
|
|
|
2932
3040
|
}
|
|
2933
3041
|
};
|
|
2934
3042
|
}
|
|
3043
|
+
function getCodexConfigPath(projectRoot3) {
|
|
3044
|
+
return path9.resolve(projectRoot3, ".codex/config.toml");
|
|
3045
|
+
}
|
|
3046
|
+
function hasCodexMcpSection(content) {
|
|
3047
|
+
return content.includes("[mcp_servers.typegraph]");
|
|
3048
|
+
}
|
|
3049
|
+
function upsertCodexMcpSection(content, block) {
|
|
3050
|
+
const sectionRe = /\n?\[mcp_servers\.typegraph\]\n[\s\S]*?(?=\n\[|$)/;
|
|
3051
|
+
const normalizedBlock = block.trim();
|
|
3052
|
+
if (sectionRe.test(content)) {
|
|
3053
|
+
const existingSection = (content.match(sectionRe)?.[0] ?? "").trim();
|
|
3054
|
+
if (existingSection === normalizedBlock) {
|
|
3055
|
+
return { content, changed: false };
|
|
3056
|
+
}
|
|
3057
|
+
const nextContent2 = content.replace(sectionRe, `
|
|
3058
|
+
${normalizedBlock}
|
|
3059
|
+
`);
|
|
3060
|
+
return { content: nextContent2.trimEnd() + "\n", changed: true };
|
|
3061
|
+
}
|
|
3062
|
+
const nextContent = content ? content.trimEnd() + "\n\n" + normalizedBlock + "\n" : normalizedBlock + "\n";
|
|
3063
|
+
return { content: nextContent, changed: true };
|
|
3064
|
+
}
|
|
3065
|
+
function makeCodexMcpBlock(projectRoot3) {
|
|
3066
|
+
const absoluteEntry = getAbsoluteMcpServerEntry(projectRoot3);
|
|
3067
|
+
return [
|
|
3068
|
+
"",
|
|
3069
|
+
"[mcp_servers.typegraph]",
|
|
3070
|
+
`command = "${absoluteEntry.command}"`,
|
|
3071
|
+
`args = ["${absoluteEntry.args[0]}", "${absoluteEntry.args[1]}"]`,
|
|
3072
|
+
`env = { TYPEGRAPH_PROJECT_ROOT = "${absoluteEntry.env.TYPEGRAPH_PROJECT_ROOT}", TYPEGRAPH_TSCONFIG = "${absoluteEntry.env.TYPEGRAPH_TSCONFIG}" }`,
|
|
3073
|
+
""
|
|
3074
|
+
].join("\n");
|
|
3075
|
+
}
|
|
3076
|
+
function isCodexProjectTrusted(projectRoot3) {
|
|
3077
|
+
const home = process.env.HOME;
|
|
3078
|
+
if (!home) return false;
|
|
3079
|
+
const globalConfigPath = path9.join(home, ".codex/config.toml");
|
|
3080
|
+
if (!fs8.existsSync(globalConfigPath)) return false;
|
|
3081
|
+
const content = fs8.readFileSync(globalConfigPath, "utf-8");
|
|
3082
|
+
const lines = content.split(/\r?\n/);
|
|
3083
|
+
let currentProject = null;
|
|
3084
|
+
let currentTrusted = false;
|
|
3085
|
+
const matchesTrustedProject = () => currentProject !== null && currentTrusted && (projectRoot3 === currentProject || projectRoot3.startsWith(currentProject + path9.sep));
|
|
3086
|
+
for (const line of lines) {
|
|
3087
|
+
const sectionMatch = line.match(/^\[projects\."([^"]+)"\]\s*$/);
|
|
3088
|
+
if (sectionMatch) {
|
|
3089
|
+
if (matchesTrustedProject()) return true;
|
|
3090
|
+
currentProject = path9.resolve(sectionMatch[1]);
|
|
3091
|
+
currentTrusted = false;
|
|
3092
|
+
continue;
|
|
3093
|
+
}
|
|
3094
|
+
if (line.startsWith("[")) {
|
|
3095
|
+
if (matchesTrustedProject()) return true;
|
|
3096
|
+
currentProject = null;
|
|
3097
|
+
currentTrusted = false;
|
|
3098
|
+
continue;
|
|
3099
|
+
}
|
|
3100
|
+
if (currentProject && /\btrust_level\s*=\s*"trusted"/.test(line)) {
|
|
3101
|
+
currentTrusted = true;
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
return matchesTrustedProject();
|
|
3105
|
+
}
|
|
2935
3106
|
function registerMcpServers(projectRoot3, selectedAgents) {
|
|
2936
3107
|
if (selectedAgents.includes("cursor")) {
|
|
2937
3108
|
registerJsonMcp(projectRoot3, ".cursor/mcp.json", "mcpServers");
|
|
@@ -2994,90 +3165,34 @@ function deregisterJsonMcp(projectRoot3, configPath, rootKey) {
|
|
|
2994
3165
|
}
|
|
2995
3166
|
}
|
|
2996
3167
|
function registerCodexMcp(projectRoot3) {
|
|
2997
|
-
const absoluteEntry = getAbsoluteMcpServerEntry(projectRoot3);
|
|
2998
|
-
const codexGet = spawnSync2("codex", ["mcp", "get", "typegraph"], {
|
|
2999
|
-
stdio: "pipe",
|
|
3000
|
-
encoding: "utf-8"
|
|
3001
|
-
});
|
|
3002
|
-
if (codexGet.status === 0) {
|
|
3003
|
-
const output = `${codexGet.stdout ?? ""}${codexGet.stderr ?? ""}`;
|
|
3004
|
-
const hasServerPath = output.includes(absoluteEntry.args[1]);
|
|
3005
|
-
const hasProjectRoot = output.includes("TYPEGRAPH_PROJECT_ROOT=*****") || output.includes(projectRoot3);
|
|
3006
|
-
const hasTsconfig = output.includes("TYPEGRAPH_TSCONFIG=*****") || output.includes(path9.resolve(projectRoot3, "tsconfig.json"));
|
|
3007
|
-
if (hasServerPath && hasProjectRoot && hasTsconfig) {
|
|
3008
|
-
p.log.info("Codex CLI: typegraph MCP server already registered");
|
|
3009
|
-
return;
|
|
3010
|
-
}
|
|
3011
|
-
spawnSync2("codex", ["mcp", "remove", "typegraph"], {
|
|
3012
|
-
stdio: "pipe",
|
|
3013
|
-
encoding: "utf-8"
|
|
3014
|
-
});
|
|
3015
|
-
}
|
|
3016
|
-
const codexAdd = spawnSync2(
|
|
3017
|
-
"codex",
|
|
3018
|
-
[
|
|
3019
|
-
"mcp",
|
|
3020
|
-
"add",
|
|
3021
|
-
"typegraph",
|
|
3022
|
-
"--env",
|
|
3023
|
-
`TYPEGRAPH_PROJECT_ROOT=${absoluteEntry.env.TYPEGRAPH_PROJECT_ROOT}`,
|
|
3024
|
-
"--env",
|
|
3025
|
-
`TYPEGRAPH_TSCONFIG=${absoluteEntry.env.TYPEGRAPH_TSCONFIG}`,
|
|
3026
|
-
"--",
|
|
3027
|
-
absoluteEntry.command,
|
|
3028
|
-
...absoluteEntry.args
|
|
3029
|
-
],
|
|
3030
|
-
{
|
|
3031
|
-
stdio: "pipe",
|
|
3032
|
-
encoding: "utf-8"
|
|
3033
|
-
}
|
|
3034
|
-
);
|
|
3035
|
-
if (codexAdd.status === 0) {
|
|
3036
|
-
p.log.success("Codex CLI: registered typegraph MCP server");
|
|
3037
|
-
return;
|
|
3038
|
-
}
|
|
3039
|
-
p.log.warn(
|
|
3040
|
-
`Codex CLI registration failed \u2014 falling back to ${".codex/config.toml"}`
|
|
3041
|
-
);
|
|
3042
3168
|
const configPath = ".codex/config.toml";
|
|
3043
|
-
const fullPath =
|
|
3169
|
+
const fullPath = getCodexConfigPath(projectRoot3);
|
|
3170
|
+
const block = makeCodexMcpBlock(projectRoot3);
|
|
3044
3171
|
let content = "";
|
|
3045
3172
|
if (fs8.existsSync(fullPath)) {
|
|
3046
3173
|
content = fs8.readFileSync(fullPath, "utf-8");
|
|
3047
|
-
if (content.includes("[mcp_servers.typegraph]")) {
|
|
3048
|
-
p.log.info(`${configPath}: typegraph MCP server already registered`);
|
|
3049
|
-
return;
|
|
3050
|
-
}
|
|
3051
3174
|
}
|
|
3052
|
-
const block = [
|
|
3053
|
-
"",
|
|
3054
|
-
"[mcp_servers.typegraph]",
|
|
3055
|
-
`command = "${absoluteEntry.command}"`,
|
|
3056
|
-
`args = ["${absoluteEntry.args[0]}", "${absoluteEntry.args[1]}"]`,
|
|
3057
|
-
`env = { TYPEGRAPH_PROJECT_ROOT = "${absoluteEntry.env.TYPEGRAPH_PROJECT_ROOT}", TYPEGRAPH_TSCONFIG = "${absoluteEntry.env.TYPEGRAPH_TSCONFIG}" }`,
|
|
3058
|
-
""
|
|
3059
|
-
].join("\n");
|
|
3060
3175
|
const dir = path9.dirname(fullPath);
|
|
3061
3176
|
if (!fs8.existsSync(dir)) {
|
|
3062
3177
|
fs8.mkdirSync(dir, { recursive: true });
|
|
3063
3178
|
}
|
|
3064
|
-
const
|
|
3065
|
-
|
|
3066
|
-
|
|
3179
|
+
const { content: nextContent, changed } = upsertCodexMcpSection(content, block);
|
|
3180
|
+
if (changed) {
|
|
3181
|
+
fs8.writeFileSync(fullPath, nextContent);
|
|
3182
|
+
p.log.success(`${configPath}: registered typegraph MCP server`);
|
|
3183
|
+
} else {
|
|
3184
|
+
p.log.info(`${configPath}: typegraph MCP server already registered`);
|
|
3185
|
+
}
|
|
3186
|
+
if (!isCodexProjectTrusted(projectRoot3)) {
|
|
3187
|
+
p.log.info(`Codex CLI: trust ${projectRoot3} in ~/.codex/config.toml to load project MCP settings`);
|
|
3188
|
+
}
|
|
3067
3189
|
}
|
|
3068
3190
|
function deregisterCodexMcp(projectRoot3) {
|
|
3069
|
-
const codexRemove = spawnSync2("codex", ["mcp", "remove", "typegraph"], {
|
|
3070
|
-
stdio: "pipe",
|
|
3071
|
-
encoding: "utf-8"
|
|
3072
|
-
});
|
|
3073
|
-
if (codexRemove.status === 0) {
|
|
3074
|
-
p.log.info("Codex CLI: removed typegraph MCP server");
|
|
3075
|
-
}
|
|
3076
3191
|
const configPath = ".codex/config.toml";
|
|
3077
|
-
const fullPath =
|
|
3192
|
+
const fullPath = getCodexConfigPath(projectRoot3);
|
|
3078
3193
|
if (!fs8.existsSync(fullPath)) return;
|
|
3079
3194
|
let content = fs8.readFileSync(fullPath, "utf-8");
|
|
3080
|
-
if (!content
|
|
3195
|
+
if (!hasCodexMcpSection(content)) return;
|
|
3081
3196
|
content = content.replace(
|
|
3082
3197
|
/\n?\[mcp_servers\.typegraph\]\n[\s\S]*?(?=\n\[|$)/,
|
|
3083
3198
|
""
|
|
@@ -3279,12 +3394,12 @@ async function setup(yes2) {
|
|
|
3279
3394
|
}
|
|
3280
3395
|
s.message("Installing dependencies...");
|
|
3281
3396
|
try {
|
|
3282
|
-
execSync2("npm install", { cwd: targetDir, stdio: "pipe" });
|
|
3397
|
+
execSync2("npm install --include=optional", { cwd: targetDir, stdio: "pipe" });
|
|
3283
3398
|
s.stop(`${isUpdate ? "Updated" : "Installed"} ${copied} files with dependencies`);
|
|
3284
3399
|
} catch (err) {
|
|
3285
3400
|
s.stop(`${isUpdate ? "Updated" : "Installed"} ${copied} files`);
|
|
3286
3401
|
p.log.warn(`Dependency install failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3287
|
-
p.log.info(`Run manually: cd ${PLUGIN_DIR_NAME} && npm install`);
|
|
3402
|
+
p.log.info(`Run manually: cd ${PLUGIN_DIR_NAME} && npm install --include=optional`);
|
|
3288
3403
|
}
|
|
3289
3404
|
if (needsAgentsSkills) {
|
|
3290
3405
|
const agentsNames = selectedAgents.filter((id) => AGENTS[id].needsAgentsSkills).map((id) => AGENTS[id].name);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typegraph-mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.33",
|
|
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",
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/guyowen/typegraph-mcp.git"
|
|
10
10
|
},
|
|
11
|
+
"packageManager": "npm@11.5.2",
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=22"
|
|
14
|
+
},
|
|
11
15
|
"bin": {
|
|
12
16
|
"typegraph-mcp": "./dist/cli.js",
|
|
13
17
|
"typegraph": "./dist/cli.js"
|
package/scripts/ensure-deps.sh
CHANGED
|
@@ -9,6 +9,14 @@ set -e
|
|
|
9
9
|
|
|
10
10
|
PLUGIN_DIR="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
|
|
11
11
|
|
|
12
|
+
if command -v node &> /dev/null; then
|
|
13
|
+
NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')"
|
|
14
|
+
if [ "$NODE_MAJOR" -lt 22 ]; then
|
|
15
|
+
echo "Warning: typegraph-mcp requires Node.js >= 22. Current: $(node -v)"
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
18
|
+
fi
|
|
19
|
+
|
|
12
20
|
# Check if node_modules exist with required packages
|
|
13
21
|
if [ -d "$PLUGIN_DIR/node_modules/@modelcontextprotocol" ] && \
|
|
14
22
|
[ -d "$PLUGIN_DIR/node_modules/oxc-parser" ] && \
|
|
@@ -21,7 +29,7 @@ echo "Installing typegraph-mcp dependencies..."
|
|
|
21
29
|
cd "$PLUGIN_DIR"
|
|
22
30
|
|
|
23
31
|
if command -v npm &> /dev/null; then
|
|
24
|
-
npm install
|
|
32
|
+
npm install --include=optional
|
|
25
33
|
else
|
|
26
34
|
echo "Warning: npm not found. Run 'npm install' in $PLUGIN_DIR manually."
|
|
27
35
|
exit 1
|