Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

diffinery setup: write Codex's diffinery server to config.toml

codex mcp add starts a browser sign-in for a server that offers OAuth and
waits for it, which hung setup. Append the table instead, after codex mcp
remove drops a server of the user's own, and check codex reads it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TdJysKcGD4ekSD5ZHbwBmJ
Claude authored signed1 parent 5d4a2f9commit 9ece7a74b9a56ec5cf080724ac802175e7ddaa55Browse files
2 files changed+56−16
modifiedapps/cli/src/commands/codex.ts+20−4View file
@@ -1,10 +1,14 @@
11 import { spawn } from "node:child_process";
2+import { appendFile, mkdir, readFile } from "node:fs/promises";
3+import { homedir } from "node:os";
4+import path from "node:path";
25
36 /**
47 * Codex's `diffinery` MCP server, pointed at a self-hosted Diffinery by `diffinery setup`. The Codex plugin's
58 * 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.
9+ * name in Codex's own config replaces it, so for another instance this adds that server to config.toml.
10+ * Not with `codex mcp add`, which starts a browser sign-in for a server that offers OAuth and waits for it.
11+ * Nothing happens for diffinery.com, or where Codex isn't installed.
812 */
913
1014 const DEFAULT_URL = "https://diffinery.com";
@@ -49,7 +53,19 @@
4953 // An answer we can't read: add the server all the same.
5054 }
5155 }
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)}` };
56+ const home = env.CODEX_HOME && path.isAbsolute(env.CODEX_HOME) ? env.CODEX_HOME : path.join(env.HOME || homedir(), ".codex");
57+ const file = path.join(home, "config.toml");
58+ const text = await readFile(file, "utf8").catch(() => "");
59+ // A server of the user's own goes first (codex removes it however it's written); the plugin's stays.
60+ if (/^\s*\[\s*mcp_servers\s*\.\s*"?diffinery"?\s*\]/m.test(text) || /^\s*mcp_servers\s*\.\s*"?diffinery"?\s*[.=]/m.test(text)) {
61+ const rm = await codex(env, ["mcp", "remove", "diffinery"]);
62+ if (rm.code !== 0) return { ok: false, message: `codex mcp remove diffinery failed: ${(rm.stderr.trim().split("\n")[0] || `exit status ${rm.code}`).slice(0, 200)}` };
63+ }
64+ const now = await readFile(file, "utf8").catch(() => "");
65+ const table = `[mcp_servers.diffinery]\nurl = ${JSON.stringify(want)}\n${bearer ? `bearer_token_env_var = ${JSON.stringify(bearer)}\n` : ""}`;
66+ await mkdir(home, { recursive: true });
67+ await appendFile(file, `${now && !now.endsWith("\n") ? "\n" : ""}${now ? "\n" : ""}${table}`);
68+ const check = await codex(env, ["mcp", "get", "diffinery", "--json"]);
69+ if (!check.stdout.includes(JSON.stringify(want).slice(1, -1))) return { ok: false, message: `Codex doesn't read the diffinery server added to ${file}: check that file` };
5470 return { ok: true, message: `the diffinery MCP server is now ${want} (sign in with: codex mcp login diffinery)` };
5571 }
modifiedapps/cli/test/setup.test.ts+36−12View file
@@ -344,39 +344,63 @@
344344 });
345345 afterEach(() => srv.close());
346346
347- /** A codex that records its calls and answers `mcp get` with `server` (none: exit status 1, as codex does). */
347+ /**
348+ * A codex that records its calls. `mcp get` answers `server` (none: exit status 1, as codex does) until
349+ * config.toml names a URL, then that URL; `mcp remove` drops the config's diffinery table.
350+ */
348351 function fakeCodex(server: unknown): { dir: string; calls: () => string[] } {
349352 const dir = tempDir("diffinery-fake-codex-");
350353 const log = path.join(dir, "calls");
351354 writeFileSync(path.join(dir, "answer.json"), JSON.stringify(server ?? null));
352355 writeFileSync(
353356 path.join(dir, "codex"),
354- `#!/bin/sh\nprintf '%s\\n' "$*" >> "${log}"\nif [ "$1 $2" = "mcp get" ]; then ${server ? `cat "${dir}/answer.json"` : "echo 'Error: No MCP server named' >&2; exit 1"}; fi\nexit 0\n`,
357+ [
358+ "#!/bin/sh",
359+ `printf '%s\\n' "$*" >> "${log}"`,
360+ 'cfg="$CODEX_HOME/config.toml"',
361+ 'if [ "$1 $2" = "mcp remove" ]; then sed -i.bak "/mcp_servers.diffinery/,\\$d" "$cfg"; exit 0; fi',
362+ 'if [ "$1 $2" = "mcp get" ]; then',
363+ ' u=$(sed -n \'s/^url = "\\(.*\\)"$/\\1/p\' "$cfg" 2>/dev/null | tail -n 1)',
364+ ' b=$(sed -n \'s/^bearer_token_env_var = "\\(.*\\)"$/\\1/p\' "$cfg" 2>/dev/null | tail -n 1)',
365+ ' if [ -n "$u" ]; then printf \'{"transport":{"url":"%s","bearer_token_env_var":"%s"}}\' "$u" "$b"; exit 0; fi',
366+ server ? ` cat "${dir}/answer.json"` : " echo 'Error: No MCP server named' >&2; exit 1",
367+ "fi",
368+ "exit 0",
369+ "",
370+ ].join("\n"),
355371 );
356372 chmodSync(path.join(dir, "codex"), 0o755);
357373 return { dir, calls: () => (existsSync(log) ? readFileSync(log, "utf8").trim().split("\n") : []) };
358374 }
359- const machine = async (codexDir: string) => {
375+ const machine = async (codexDir: string, config?: string) => {
360376 const env = isolatedEnv({ DIFFINERY_NO_CODEX: "", PATH: `${codexDir}${path.delimiter}${process.env.PATH}` });
377+ env.CODEX_HOME = path.join(env.HOME!, ".codex");
378+ if (config !== undefined) {
379+ mkdirSync(env.CODEX_HOME);
380+ writeFileSync(path.join(env.CODEX_HOME, "config.toml"), config);
381+ }
361382 await writeConfig(env, { url: srv.url, token: "dfy_good" });
362383 return env;
363384 };
385+ const tomlOf = (env: NodeJS.ProcessEnv) => readFileSync(path.join(env.CODEX_HOME!, "config.toml"), "utf8");
364386
365387 it("points Codex's diffinery MCP server at a self-hosted server, keeping a headless token setting", async () => {
366388 const plugin = fakeCodex({ name: "diffinery", transport: { type: "streamable_http", url: "https://diffinery.com/mcp", bearer_token_env_var: null } });
367- const r = await run(["setup", "--no-auto-merge"], { env: await machine(plugin.dir) });
389+ const env = await machine(plugin.dir, 'model = "o3"');
390+ const r = await run(["setup", "--no-auto-merge"], { env });
368391 expect(r.code).toBe(0);
369392 expect(r.stdout).toContain(`✓ Codex: the diffinery MCP server is now ${srv.url}/mcp`);
370- expect(plugin.calls()).toEqual(["mcp get diffinery --json", `mcp add diffinery --url ${srv.url}/mcp`]);
393+ expect(plugin.calls()).toEqual(["mcp get diffinery --json", "mcp get diffinery --json"]);
394+ expect(tomlOf(env)).toBe(`model = "o3"\n\n[mcp_servers.diffinery]\nurl = "${srv.url}/mcp"\n`);
395+ // Again: nothing to change.
396+ expect((await run(["setup", "--no-auto-merge"], { env })).stdout).toContain(`✓ Codex: the diffinery MCP server is ${srv.url}/mcp`);
371397
398+ // A server of the user's own, sending DIFFINERY_TOKEN: replaced, and still sending it.
372399 const headless = fakeCodex({ name: "diffinery", transport: { url: "https://old.example/mcp", bearer_token_env_var: "DIFFINERY_TOKEN" } });
373- await run(["setup", "--no-auto-merge"], { env: await machine(headless.dir) });
374- expect(headless.calls()[1]).toBe(`mcp add diffinery --url ${srv.url}/mcp --bearer-token-env-var DIFFINERY_TOKEN`);
375-
376- const done = fakeCodex({ name: "diffinery", transport: { url: `${srv.url}/mcp` } });
377- const again = await run(["setup", "--no-auto-merge"], { env: await machine(done.dir) });
378- expect(again.stdout).toContain(`✓ Codex: the diffinery MCP server is ${srv.url}/mcp`);
379- expect(done.calls()).toEqual(["mcp get diffinery --json"]);
400+ const henv = await machine(headless.dir, '[mcp_servers.diffinery]\nurl = "https://old.example/mcp"\nbearer_token_env_var = "DIFFINERY_TOKEN"\n');
401+ await run(["setup", "--no-auto-merge"], { env: henv });
402+ expect(headless.calls()).toContain("mcp remove diffinery");
403+ expect(tomlOf(henv)).toBe(`[mcp_servers.diffinery]\nurl = "${srv.url}/mcp"\nbearer_token_env_var = "DIFFINERY_TOKEN"\n`);
380404 });
381405
382406 it("leaves Codex alone for diffinery.com", async () => {