#!/usr/bin/env node import { mkdir, readFile, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; const FOUNDRY_SERVER_NAME = "nodedc_module_foundry"; const ONTOLOGY_SERVER_NAME = "nodedc_ontology"; const SKILL_NAME = "foundry-context"; const MCP_PROTOCOL_VERSION = "2025-06-18"; function usage(message = "") { if (message) console.error(message); console.log(`Usage: nodedc-foundry-codex-agent --server --code nodedc-foundry-codex-agent doctor [--codex-home ] The setup command is issued by Foundry Settings → Codex Agent API.`); process.exitCode = message ? 1 : 0; } function parseArgs(raw) { const args = raw[0] === "--" ? raw.slice(1) : raw; const out = { command: "setup", server: "", code: "", codexHome: process.env.CODEX_HOME || "" }; if (args[0] === "doctor") out.command = "doctor"; const values = out.command === "doctor" ? args.slice(1) : args; for (let index = 0; index < values.length; index += 1) { const item = values[index]; if (item === "--server") out.server = String(values[++index] || ""); else if (item.startsWith("--server=")) out.server = item.slice("--server=".length); else if (item === "--code") out.code = String(values[++index] || ""); else if (item.startsWith("--code=")) out.code = item.slice("--code=".length); else if (item === "--codex-home") out.codexHome = String(values[++index] || ""); else if (item.startsWith("--codex-home=")) out.codexHome = item.slice("--codex-home=".length); else if (item === "--help" || item === "-h") out.help = true; else throw new Error(`unknown_argument:${item}`); } out.server = String(out.server || "").replace(/\/+$/, ""); return out; } function codexHome(value) { if (!value) return path.join(os.homedir(), ".codex"); if (value === "~") return os.homedir(); if (value.startsWith("~/") || value.startsWith("~\\")) return path.join(os.homedir(), value.slice(2)); return path.resolve(value); } async function readIfExists(file) { try { return await readFile(file, "utf8"); } catch (error) { if (error?.code === "ENOENT") return null; throw error; } } function stripMcpBlocks(content, serverNames) { const targets = serverNames.map((name) => `mcp_servers.${name}`); let skip = false; const kept = []; for (const line of String(content || "").split(/\r?\n/)) { const section = line.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/)?.[1]?.trim() || ""; if (section) skip = targets.some((target) => section === target || section.startsWith(`${target}.`)); if (!skip) kept.push(line); } return kept.join("\n").trimEnd(); } function mcpBlock(serverName, endpoint, token) { return [ `[mcp_servers.${serverName}]`, `url = ${JSON.stringify(endpoint)}`, "enabled = true", "required = false", "startup_timeout_sec = 30", "tool_timeout_sec = 120", "", `[mcp_servers.${serverName}.http_headers]`, `Authorization = ${JSON.stringify(`Bearer ${token}`)}`, `Accept = ${JSON.stringify("application/json, text/event-stream")}`, `${JSON.stringify("MCP-Protocol-Version")} = ${JSON.stringify(MCP_PROTOCOL_VERSION)}`, ].join("\n"); } async function writeConfig(home, setup) { const configPath = path.join(home, "config.toml"); await mkdir(path.dirname(configPath), { recursive: true }); const original = await readIfExists(configPath); if (original !== null) await writeFile(`${configPath}.nodedc-foundry-agent.bak`, original, "utf8"); const prefix = stripMcpBlocks(original || "", [FOUNDRY_SERVER_NAME, ONTOLOGY_SERVER_NAME]); const blocks = [ mcpBlock(FOUNDRY_SERVER_NAME, setup.foundry.mcpUrl, setup.foundry.token), mcpBlock(ONTOLOGY_SERVER_NAME, setup.ontology.mcpUrl, setup.ontology.token), ].join("\n\n"); await writeFile(configPath, `${prefix}${prefix ? "\n\n" : ""}${blocks}\n`, "utf8"); return configPath; } function skillBody() { return `--- name: foundry-context description: Use for NODE.DC Module Foundry application work. Use Foundry and the separate read-only Ontology MCP; never depend on Foundry source files. --- # NODE.DC Module Foundry - Treat \`nodedc_module_foundry\` as the only authority for Foundry application instances and Page Library bindings. Do not read or edit Foundry source, runtime files or manifests directly. - Treat \`nodedc_ontology\` as a separate read-only semantic authority. Use it before inventing entity ids, semantic types, aliases, relations or routing assumptions. - Work only inside the currently granted Foundry user contour. Sharing, cross-user visibility, roles and public release are outside this connector until explicit tools expose them. - Canonical Page Library templates are immutable. Create and modify application instances through Foundry tools only. - Use a fresh idempotency key for every write. Reuse a key only when intentionally replaying the exact same operation. - Provider URLs, credentials, polling and normalization stay in NDC Engine L2. Foundry consumes provider-neutral Data Products and semantic bindings only. - Never request or place provider tokens, internal workload grants or raw secret values in an application, page binding, prompt or MCP argument. - For the first Map slice, resolve semantics through Ontology, create the application/page in Foundry, bind the approved Data Product, and verify the resulting application manifest through Foundry MCP reads. `; } async function writeSkill(home) { const file = path.join(home, "skills", SKILL_NAME, "SKILL.md"); await mkdir(path.dirname(file), { recursive: true }); await writeFile(file, skillBody(), "utf8"); return file; } async function redeem(server, code) { const response = await fetch(`${server}/api/foundry-agent/v1/setup/redeem`, { method: "POST", headers: { accept: "application/json", "content-type": "application/json" }, body: JSON.stringify({ code, deviceName: `${os.hostname()} Codex Desktop` }), }); const payload = await response.json().catch(() => ({})); if (!response.ok || !payload?.ok || !payload?.foundry?.token || !payload?.ontology?.token) { throw new Error(payload?.error || `setup_redeem_failed_${response.status}`); } return payload; } async function toolsSmoke(serverName, endpoint, token) { const headers = { accept: "application/json, text/event-stream", authorization: `Bearer ${token}`, "content-type": "application/json", "mcp-protocol-version": MCP_PROTOCOL_VERSION, }; const initialize = await fetch(endpoint, { method: "POST", headers, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: MCP_PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: "nodedc-foundry-setup", version: "0.1.0" } }, }), }); if (!initialize.ok) throw new Error(`${serverName}_initialize_failed_${initialize.status}`); const list = await fetch(endpoint, { method: "POST", headers, body: JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }), }); const payload = await list.json().catch(() => ({})); if (!list.ok || !Array.isArray(payload?.result?.tools)) throw new Error(`${serverName}_tools_list_failed_${list.status}`); return payload.result.tools.length; } async function main() { const args = parseArgs(process.argv.slice(2)); if (args.help) return usage(); const home = codexHome(args.codexHome); if (args.command === "doctor") { const config = await readIfExists(path.join(home, "config.toml")); const skill = await readIfExists(path.join(home, "skills", SKILL_NAME, "SKILL.md")); const ok = Boolean( config?.includes(`[mcp_servers.${FOUNDRY_SERVER_NAME}]`) && config?.includes(`[mcp_servers.${ONTOLOGY_SERVER_NAME}]`) && skill, ); console.log(ok ? "NODE.DC Foundry + Ontology Codex install is present." : "NODE.DC Foundry + Ontology Codex install is incomplete."); process.exitCode = ok ? 0 : 1; return; } if (!args.server || !args.code) return usage("server_and_code_required"); const setup = await redeem(args.server, args.code); const configPath = await writeConfig(home, setup); const skillPath = await writeSkill(home); const foundryToolCount = await toolsSmoke(FOUNDRY_SERVER_NAME, setup.foundry.mcpUrl, setup.foundry.token); const ontologyToolCount = await toolsSmoke(ONTOLOGY_SERVER_NAME, setup.ontology.mcpUrl, setup.ontology.token); console.log("NODE.DC Foundry + Ontology Codex setup complete."); console.log("Config:", configPath); console.log("Skill:", skillPath); console.log("Foundry MCP smoke tools:", foundryToolCount); console.log("Ontology MCP smoke tools:", ontologyToolCount); console.log("Restart Codex Desktop completely before using the new MCP tools."); } main().catch((error) => { console.error(`NODE.DC Foundry Codex setup failed: ${error?.message || error}`); process.exitCode = 1; });