| 1 | import { spawn } from "node:child_process"; |
| 2 | |
| 3 | /** |
| 4 | * Codex's `diffinery` MCP server, pointed at a self-hosted Diffinery by `diffinery setup`. The Codex plugin's |
| 5 | * server is https://diffinery.com/mcp (Codex has no plugin settings to change it), and a server of the same |
| 6 | * name in Codex's own config replaces it, so for another instance this adds that server with |
| 7 | * `codex mcp add`. Nothing happens for diffinery.com, or where Codex isn't installed. |
| 8 | */ |
| 9 | |
| 10 | const DEFAULT_URL = "https://diffinery.com"; |
| 11 | |
| 12 | function codex(env: NodeJS.ProcessEnv, args: string[]): Promise<{ code: number; stdout: string; stderr: string }> { |
| 13 | return new Promise((resolve) => { |
| 14 | let child; |
| 15 | try { |
| 16 | child = spawn("codex", args, { env, stdio: ["ignore", "pipe", "pipe"] }); |
| 17 | } catch (e) { |
| 18 | return resolve({ code: 127, stdout: "", stderr: (e as Error).message }); |
| 19 | } |
| 20 | let stdout = ""; |
| 21 | let stderr = ""; |
| 22 | child.stdout.on("data", (d: Buffer) => (stdout += d.toString())); |
| 23 | child.stderr.on("data", (d: Buffer) => (stderr += d.toString())); |
| 24 | child.on("error", (e) => resolve({ code: 127, stdout, stderr: e.message })); |
| 25 | child.on("close", (code) => resolve({ code: code ?? 1, stdout, stderr })); |
| 26 | }); |
| 27 | } |
| 28 | |
| 29 | export interface CodexResult { |
| 30 | ok: boolean; |
| 31 | message: string; |
| 32 | } |
| 33 | |
| 34 | /** Null when there's nothing to do: diffinery.com, DIFFINERY_NO_CODEX, or no `codex` on PATH. */ |
| 35 | export async function setupCodexMcp(env: NodeJS.ProcessEnv, url: string): Promise<CodexResult | null> { |
| 36 | if (url === DEFAULT_URL || env.DIFFINERY_NO_CODEX) return null; |
| 37 | const want = `${url}/mcp`; |
| 38 | // The server Codex uses now: the user's own, else the plugin's, else none (exit status 1). |
| 39 | const got = await codex(env, ["mcp", "get", "diffinery", "--json"]); |
| 40 | if (got.code === 127) return null; |
| 41 | let bearer: string | null = null; |
| 42 | if (got.code === 0) { |
| 43 | try { |
| 44 | const t = (JSON.parse(got.stdout) as { transport?: { url?: unknown; bearer_token_env_var?: unknown } }).transport ?? {}; |
| 45 | if (t.url === want) return { ok: true, message: `the diffinery MCP server is ${want}` }; |
| 46 | // A headless setup that sends DIFFINERY_TOKEN keeps doing so. |
| 47 | if (typeof t.bearer_token_env_var === "string") bearer = t.bearer_token_env_var; |
| 48 | } catch { |
| 49 | // An answer we can't read: add the server all the same. |
| 50 | } |
| 51 | } |
| 52 | const add = await codex(env, ["mcp", "add", "diffinery", "--url", want, ...(bearer ? ["--bearer-token-env-var", bearer] : [])]); |
| 53 | if (add.code !== 0) return { ok: false, message: `codex mcp add failed: ${(add.stderr.trim().split("\n")[0] || `exit status ${add.code}`).slice(0, 200)}` }; |
| 54 | return { ok: true, message: `the diffinery MCP server is now ${want} (sign in with: codex mcp login diffinery)` }; |
| 55 | } |