#!/usr/bin/env node import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; import { readSettings, generateManifest } from '../src/generate.js'; const RESET = '\x1b[0m'; const BOLD = '\x1b[1m'; const RED = '\x1b[31m'; const GREEN = '\x1b[32m'; const YELLOW = '\x1b[33m'; const CYAN = '\x1b[36m'; const DIM = '\x1b[2m'; const args = process.argv.slice(2); if (args.includes('--help') || args.includes('-h')) { console.log(` ${BOLD}mcp-manifest-generate${RESET} — Generate mcp-manifest.json from existing MCP server configs ${BOLD}Usage:${RESET} mcp-manifest-generate List servers in default settings mcp-manifest-generate --server Generate manifest for a specific server mcp-manifest-generate --init Interactive wizard (from scratch) mcp-manifest-generate --from-settings Use a specific settings file mcp-manifest-generate --server --probe Also run MCP handshake for server info mcp-manifest-generate --server -o Write manifest to file mcp-manifest-generate --all Generate manifests for all servers ${BOLD}Examples:${RESET} mcp-manifest-generate --init mcp-manifest-generate --server ironlicensing mcp-manifest-generate --from-settings ~/.claude/settings.json --server gitcaddy --probe mcp-manifest-generate --server ironlicensing -o mcp-manifest.json mcp-manifest-generate --all -o manifests/ ${BOLD}Options:${RESET} --init Interactive wizard to create a manifest from scratch --from-settings Path to settings.json (default: ~/.claude/settings.json) --server Server name to generate manifest for --all Generate manifests for all servers --probe Run MCP initialize handshake for server name/version -o, --output Output file or directory --json Output raw JSON (no colors/decoration) --help Show this help `); process.exit(0); } function getArg(name) { const idx = args.indexOf(name); return idx >= 0 && idx + 1 < args.length ? args[idx + 1] : null; } const settingsPath = getArg('--from-settings') || join(homedir(), '.claude', 'settings.json'); const serverName = getArg('--server'); const outputPath = getArg('-o') || getArg('--output'); const doProbe = args.includes('--probe'); const doAll = args.includes('--all'); const doInit = args.includes('--init'); const jsonOnly = args.includes('--json'); async function run() { // Init wizard mode if (doInit) { await runInitWizard(); return; } // Load settings if (!existsSync(settingsPath)) { console.error(`${RED}Settings file not found: ${settingsPath}${RESET}`); console.error(`${DIM}Default location: ~/.claude/settings.json${RESET}`); process.exit(1); } let servers; try { servers = readSettings(settingsPath); } catch (e) { console.error(`${RED}Failed to read settings: ${e.message}${RESET}`); process.exit(1); } const serverNames = Object.keys(servers); if (serverNames.length === 0) { console.error(`${YELLOW}No MCP servers found in ${settingsPath}${RESET}`); process.exit(1); } // List mode — no --server or --all specified if (!serverName && !doAll) { console.log(`\n${BOLD}MCP servers in ${DIM}${settingsPath}${RESET}\n`); for (const name of serverNames) { const entry = servers[name]; const cmd = entry.command || '(no command)'; const argCount = (entry.args || []).length; const envCount = Object.keys(entry.env || {}).length; console.log(` ${CYAN}${name}${RESET}`); console.log(` command: ${cmd}`); if (argCount > 0) console.log(` args: ${argCount} argument(s)`); if (envCount > 0) console.log(` env: ${envCount} variable(s)`); console.log(''); } console.log(`${DIM}Use --server to generate a manifest, or --all for all servers${RESET}\n`); process.exit(0); } // Generate for specific server if (serverName) { if (!servers[serverName]) { console.error(`${RED}Server "${serverName}" not found. Available: ${serverNames.join(', ')}${RESET}`); process.exit(1); } const manifest = await generateManifest(serverName, servers[serverName], { probe: doProbe }); outputManifest(manifest, serverName, outputPath, jsonOnly); return; } // Generate for all servers if (doAll) { for (const name of serverNames) { if (!jsonOnly) console.log(`\n${BOLD}Generating: ${CYAN}${name}${RESET}`); const manifest = await generateManifest(name, servers[name], { probe: doProbe }); const outPath = outputPath ? join(outputPath, `${name}.mcp-manifest.json`) : null; outputManifest(manifest, name, outPath, jsonOnly); } return; } } function outputManifest(manifest, name, outPath, jsonOnly) { const json = JSON.stringify(manifest, null, 2); if (outPath) { // Ensure directory exists for --all mode const dir = outPath.endsWith('.json') ? undefined : outPath; if (dir) { try { mkdirSync(dir, { recursive: true }); } catch {} } const filePath = outPath.endsWith('.json') ? outPath : join(outPath, `${name}.mcp-manifest.json`); writeFileSync(filePath, json + '\n'); if (!jsonOnly) console.log(`${GREEN}✓${RESET} Written to ${filePath}`); return; } if (jsonOnly) { console.log(json); return; } console.log(`\n${BOLD}Generated mcp-manifest.json for "${name}":${RESET}\n`); // Colorize JSON output const colorized = json .replace(/"([^"]+)":/g, `"${CYAN}$1${RESET}":`) .replace(/: "([^"]+)"/g, `: "${GREEN}$1${RESET}"`) .replace(/: (\d+)/g, `: ${YELLOW}$1${RESET}`); console.log(colorized); console.log(`\n${DIM}Review and edit the TODO fields, then save as mcp-manifest.json${RESET}`); console.log(`${DIM}Validate with: mcp-manifest-validate ./mcp-manifest.json${RESET}\n`); } async function runInitWizard() { const { createInterface } = await import('readline'); const rl = createInterface({ input: process.stdin, output: process.stdout }); const ask = (q, def) => new Promise(resolve => { const prompt = def ? `${q} ${DIM}(${def})${RESET}: ` : `${q}: `; rl.question(prompt, answer => resolve(answer.trim() || def || '')); }); console.log(`\n${BOLD}MCP Manifest Generator${RESET}`); console.log(`${DIM}Create a new mcp-manifest.json interactively\n${RESET}`); // Server info console.log(`${BOLD}Server Info${RESET}`); const name = await ask(' Server name (lowercase, hyphens)', ''); const displayName = await ask(' Display name', name.split('-').map(w => w[0]?.toUpperCase() + w.slice(1)).join(' ')); const description = await ask(' Description', ''); const version = await ask(' Version', '1.0.0'); const author = await ask(' Author/organization', ''); // Install console.log(`\n${BOLD}Installation${RESET}`); console.log(` ${DIM}Methods: npm, pip, dotnet-tool, cargo, binary, docker${RESET}`); const method = await ask(' Install method', 'npm'); const pkg = await ask(' Package name', name); const command = await ask(' Command after install', name); const source = await ask(' Custom registry URL (optional)', ''); // Transport console.log(`\n${BOLD}Transport${RESET}`); console.log(` ${DIM}Options: stdio, sse, streamable-http${RESET}`); const transport = await ask(' Transport', 'stdio'); let endpoint = ''; if (transport !== 'stdio') { endpoint = await ask(' Endpoint URL', ''); } // Config console.log(`\n${BOLD}Configuration${RESET}`); console.log(` ${DIM}Add config parameters your server needs. Empty name to finish.${RESET}`); const config = []; while (true) { const key = await ask(` Config key (empty to finish)`, ''); if (!key) break; console.log(` ${DIM}Types: string, secret, url, path, boolean, number${RESET}`); const type = await ask(` Type`, 'string'); const configDesc = await ask(` Description`, ''); const required = (await ask(` Required? (y/n)`, 'n')).toLowerCase() === 'y'; const envVar = await ask(` Environment variable (optional)`, ''); const arg = await ask(` CLI argument (optional, e.g. --api-key)`, ''); const promptText = await ask(` User prompt text`, configDesc || key); const entry = { key, description: configDesc, type, required, prompt: promptText }; if (envVar) entry.env_var = envVar; if (arg) entry.arg = arg; config.push(entry); } // Scope console.log(`\n${BOLD}Scope${RESET}`); const scope = await ask(' Scope (global, project, both)', 'global'); rl.close(); // Build manifest const manifest = { $schema: 'https://mcp-manifest.dev/schema/v0.1.json', version: '0.1', server: { name, displayName, description, version, ...(author && { author }) }, install: [{ method, package: pkg, command, ...(source && { source }), priority: 0 }], transport, ...(endpoint && { endpoint }), ...(config.length > 0 && { config }), scopes: [scope], settings_template: { command, args: config.filter(c => c.arg).flatMap(c => [c.arg, `\${${c.key}}`]) } }; const json = JSON.stringify(manifest, null, 2); if (outputPath) { writeFileSync(outputPath, json + '\n'); console.log(`\n${GREEN}✓${RESET} Written to ${outputPath}`); } else { console.log(`\n${BOLD}Generated mcp-manifest.json:${RESET}\n`); console.log(json); console.log(`\n${DIM}Save as mcp-manifest.json and validate with: mcp-manifest-validate ./mcp-manifest.json${RESET}\n`); } } run().catch(err => { console.error(`${RED}Fatal: ${err.message}${RESET}`); process.exit(2); });