// ChatGPT MCP Server — Local Setup Helper // A simple interactive onboarding script. No external dependencies. // Usage: npm run setup import readline from "node:readline"; import fs from "node:fs"; import path from "node:path"; import tty from "node:tty"; const SUPPORTED_PROVIDERS = ["openai", "manual", "ollama"]; const ROOT_DIR = path.resolve(import.meta.dirname, ".."); // ── Helpers ─────────────────────────────────────────────── function maskedInput(prompt) { return new Promise((resolve) => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); // Detect if stdin is a TTY (interactive terminal). const isTTY = process.stdin.isTTY && typeof process.stdin.setRawMode === "function"; if (!isTTY) { console.log("\n⚠ Non-interactive terminal detected — input will be visible."); } rl.question(prompt + ": ", (answer) => resolve(answer)); }); } function getProviderDefault() { const envPath = path.join(ROOT_DIR, ".env"); if (fs.existsSync(envPath)) { const content = fs.readFileSync(envPath, "utf-8"); const match = content.match(/^CHATGPT_MCP_PROVIDER\s*=\s*(\S+)/im); if (match) return match[1]; } return null; } function promptProvider() { const existing = getProviderDefault(); console.log("\nSelect a provider:"); console.log(" [1] openai — Automated second-opinion queries (requires API key)"); console.log(" [2] manual — Copy-paste prompts into ChatGPT Web (no API key needed)"); console.log(" [3] ollama — Local AI via Ollama (no API key needed)"); return new Promise((resolve) => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const hint = existing && SUPPORTED_PROVIDERS.includes(existing) ? ` (default: ${existing})` : ""; rl.question(`\nProvider${hint}: `, (input) => { rl.close(); // Allow default by empty input if (!input.trim()) { return resolve(existing || "openai"); } const map = { 1: "openai", 2: "manual", 3: "ollama" }; const selected = map[input.trim()]; if (SUPPORTED_PROVIDERS.includes(selected)) { return resolve(selected); } console.log(`\nInvalid selection: ${input.trim()}. Try again.\n`); return resolve(promptProvider()); // retry }); }); } function promptRequired(label, defaultVal) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise((resolve) => { function ask() { const hint = defaultVal ? ` (default: ${defaultVal})` : ""; rl.question(`${label}${hint}: `, (answer) => { if (defaultVal && !answer.trim()) { rl.close(); return resolve(defaultVal); } if (!answer.trim()) { console.log(" This field is required. Please enter a value."); return ask(); } rl.close(); resolve(answer.trim()); }); } ask(); }); } function promptOptional(label, defaultVal) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise((resolve) => { const hint = defaultVal ? ` (default: ${defaultVal})` : ""; rl.question(`${label}${hint}: `, (answer) => { rl.close(); if (!answer.trim() && defaultVal) { return resolve(defaultVal); } return resolve(answer.trim()); }); }); } function promptConfirm(message) { return new Promise((resolve) => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.question(`${message} [y/N]: `, (answer) => { rl.close(); resolve(["y", "yes"].includes(answer.trim().toLowerCase())); }); }); } // ── .env generation ─────────────────────────────────────── function buildEnvContent(provider, config) { const lines = []; // Provider section header lines.push(`# Chat provider: "openai", "manual", or "ollama"`); lines.push(`CHATGPT_MCP_PROVIDER=${provider}`); lines.push(""); if (provider === "openai") { lines.push("# OpenAI provider settings"); lines.push(`OPENAI_API_KEY=${config.openaiApiKey || ""}`); if (config.openaiModel) lines.push(`OPENAI_MODEL=${config.openaiModel}`); lines.push(""); } if (provider === "ollama") { lines.push("# Ollama provider settings"); if (config.ollamaBaseUrl) lines.push(`OLLAMA_BASE_URL=${config.ollamaBaseUrl}`); if (config.ollamaModel) lines.push(`OLLAMA_MODEL=${config.ollamaModel}`); if (config.ollamaTemperature !== undefined && config.ollamaTemperature !== null) lines.push(`OLLAMA_TEMPERATURE=${config.ollamaTemperature}`); if (config.ollamaTimeout !== undefined && config.ollamaTimeout !== null) lines.push(`OLLAMA_TIMEOUT=${config.ollamaTimeout}`); lines.push(""); } // Append existing .env content that doesn't conflict with our keys. const envPath = path.join(ROOT_DIR, ".env"); if (fs.existsSync(envPath)) { const existingContent = fs.readFileSync(envPath, "utf-8"); const existingKeys = new Set([ "CHATGPT_MCP_PROVIDER", "OPENAI_API_KEY", "OPENAI_MODEL", "OLLAMA_BASE_URL", "OLLAMA_MODEL", "OLLAMA_TEMPERATURE", "OLLAMA_TIMEOUT", ]); const otherLines = existingContent .split("\n") .filter((line) => { if (line.trim().startsWith("#")) return true; const keyMatch = line.match(/^(\w+)/); if (!keyMatch) return true; return !existingKeys.has(keyMatch[1]); }) // Deduplicate: keep last occurrence of each non-comment line .reduceRight((acc, line) => { const trimmed = line.trim(); if (trimmed && acc.includes(trimmed)) return acc; if (trimmed === "") return acc; return [line, ...acc]; }, []); if (otherLines.length > 0) { lines.push("# ── Other settings (preserved from existing .env) ──"); lines.push(...otherLines); } } // Ensure trailing newline return lines.join("\n") + "\n"; } // ── Claude config generation ────────────────────────────── function buildClaudeConfig() { return JSON.stringify( { mcpServers: { "chatgpt-mcp": { command: "npm", args: ["start"], }, }, }, null, 2, ); } // ── Exports for testing ───────────────────────────────── export { SUPPORTED_PROVIDERS, buildEnvContent, buildClaudeConfig }; // ── Main flow ───────────────────────────────────────────── async function main() { console.log("\n╔══════════════════════════════════════════════╗"); console.log("║ ChatGPT MCP Server — Local Setup Helper ║"); console.log("╚══════════════════════════════════════════════╝\n"); // Step 1: Provider selection const provider = await promptProvider(); console.log(`\n✓ Selected provider: ${provider}`); // Step 2: Provider-specific config prompts let config; if (provider === "openai") { config = { openaiApiKey: "", openaiModel: null }; // Mask the API key input by disabling terminal echo temporarily. const isTTY = process.stdin.isTTY && typeof process.stdin.setRawMode === "function"; if (isTTY) { process.stdin.setRawMode(true); process.stdout.write("\n"); } const rlKey = readline.createInterface({ input: process.stdin, output: process.stdout }); config.openaiApiKey = await new Promise((resolve) => { console.log("\nOPENAI_API_KEY (required — not shown as you type):"); rlKey.question(" Key: ", resolve); }); if (isTTY) process.stdin.setRawMode(false); rlKey.close(); process.stdout.write("\n"); // newline after hidden input config.openaiModel = await promptOptional("OPENAI_MODEL", "gpt-5.1"); } if (provider === "ollama") { config = {}; config.ollamaBaseUrl = await promptOptional("OLLAMA_BASE_URL", "http://localhost:11434"); config.ollamaModel = await promptRequired("OLLAMA_MODEL", "qwen3.6:35b-a3b"); config.ollamaTemperature = await promptOptional("OLLAMA_TEMPERATURE", "0.2"); config.ollamaTimeout = await promptOptional("OLLAMA_TIMEOUT", "60"); } // manual provider — no prompts needed (config is empty object) if (provider === "manual") { config = {}; } // Step 3: Generate and confirm .env const envContent = buildEnvContent(provider, config); console.log("\n" + "─".repeat(52)); console.log("The following will be written to .env:\n"); process.stdout.write(envContent); console.log("─".repeat(52) + "\n"); const confirmEnv = await promptConfirm("Overwrite .env with this configuration?"); if (!confirmEnv) { console.log("\n✗ Setup cancelled. No changes were made."); process.exit(0); } const envPath = path.join(ROOT_DIR, ".env"); fs.writeFileSync(envPath, envContent, "utf-8"); console.log("✓ .env updated.\n"); // Step 4: Optionally create Claude Code config const claudeDir = path.join(ROOT_DIR, ".claude"); const claudeConfigPath = path.join(claudeDir, "settings.local.json"); let confirmClaude = false; if (fs.existsSync(claudeConfigPath)) { console.log(`⚠ ${claudeConfigPath} already exists.`); confirmClaude = await promptConfirm("Overwrite it?"); } else { console.log(".claude/ directory not found — will be created."); // Check if .claude dir itself exists but settings doesn't if (!fs.existsSync(claudeDir)) { console.log("Creating .claude/ directory..."); fs.mkdirSync(claudeDir, { recursive: true }); } confirmClaude = await promptConfirm("Create .claude/settings.local.json?"); } if (confirmClaude) { if (!fs.existsSync(claudeDir)) { fs.mkdirSync(claudeDir, { recursive: true }); } fs.writeFileSync(claudeConfigPath, buildClaudeConfig() + "\n", "utf-8"); console.log("✓ .claude/settings.local.json created.\n"); } else { console.log(".claude/settings.local.json was skipped.\n"); } // Done console.log("╔══════════════════════════════════════════════╗"); console.log("║ Setup complete! ║"); console.log("╚══════════════════════════════════════════════╝\n"); console.log("Next steps:"); console.log(" npm test — Run the test suite"); console.log(" npm start — Start the MCP server"); console.log(` CHATGPT_MCP_PROVIDER=${provider} (active provider)`); console.log(""); if (provider === "ollama") { console.log("Tip: verify your Ollama setup with: ollama list"); console.log(""); } } main().catch((err) => { console.error(`Setup failed: ${err.message}`); process.exit(1); });