Files
chatgpt-mcp/scripts/setup.js
T

322 lines
11 KiB
JavaScript

// 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);
});