#!/usr/bin/env node import { spawn } from 'node:child_process'; import fsPromises from 'node:fs/promises'; import { printCallHelp, handleCall as runHandleCall } from './cli/call-command.js'; import { buildGlobalContext } from './cli/cli-factory.js'; import { inferCommandRouting } from './cli/command-inference.js'; import { handleConfigCli } from './cli/config-command.js'; import { handleDaemonCli } from './cli/daemon-command.js'; import { handleEmitTs } from './cli/emit-ts-command.js'; import { extractEphemeralServerFlags } from './cli/ephemeral-flags.js'; import { prepareEphemeralServerTarget } from './cli/ephemeral-target.js'; import { CliUsageError } from './cli/errors.js'; import { handleGenerateCli } from './cli/generate-cli-runner.js'; import { looksLikeHttpUrl } from './cli/http-utils.js'; import { handleInspectCli } from './cli/inspect-cli-command.js'; import { buildConnectionIssueEnvelope } from './cli/json-output.js'; import { handleList, printListHelp } from './cli/list-command.js'; import { logError, logInfo, logWarn } from './cli/logger-context.js'; import { consumeOutputFormat } from './cli/output-format.js'; import { DEBUG_HANG, dumpActiveHandles, terminateChildProcesses } from './cli/runtime-debug.js'; import { boldText, dimText, extraDimText, supportsAnsiColor } from './cli/terminal.js'; import { resolveConfigPath } from './config.js'; import { DaemonClient } from './daemon/client.js'; import { createKeepAliveRuntime } from './daemon/runtime-wrapper.js'; import { analyzeConnectionError } from './error-classifier.js'; import { isKeepAliveServer } from './lifecycle.js'; import { clearOAuthCaches } from './oauth-persistence.js'; import { createRuntime, MCPORTER_VERSION } from './runtime.js'; export { parseCallArguments } from './cli/call-arguments.js'; export { handleCall } from './cli/call-command.js'; export { handleGenerateCli } from './cli/generate-cli-runner.js'; export { handleInspectCli } from './cli/inspect-cli-command.js'; export { extractListFlags, handleList } from './cli/list-command.js'; export { resolveCallTimeout } from './cli/timeouts.js'; export async function runCli(argv) { const args = [...argv]; if (args.length === 0) { printHelp(); process.exit(1); return; } const context = buildGlobalContext(args); if ('exit' in context) { process.exit(context.code); return; } const { globalFlags, runtimeOptions } = context; const command = args.shift(); if (!command) { printHelp(); process.exit(1); return; } if (isHelpToken(command)) { printHelp(); process.exitCode = 0; return; } if (isVersionToken(command)) { await printVersion(); return; } // Early-exit command handlers that don't require runtime inference. if (command === 'generate-cli') { await handleGenerateCli(args, globalFlags); return; } if (command === 'inspect-cli') { await handleInspectCli(args); return; } const rootOverride = globalFlags['--root']; const configPath = runtimeOptions.configPath ?? globalFlags['--config']; const configResolution = resolveConfigPath(globalFlags['--config'], rootOverride ?? process.cwd()); const configPathResolved = configPath ?? configResolution.path; // Only pass configPath to runtime options if it was explicitly provided (via --config flag or env var). // If not explicit, let loadConfigLayers handle the default resolution to avoid ENOENT on missing config. const runtimeOptionsWithPath = { ...runtimeOptions, configPath: configResolution.explicit ? configPathResolved : runtimeOptions.configPath, }; if (command === 'daemon') { await handleDaemonCli(args, { configPath: configPathResolved, configExplicit: configResolution.explicit, rootDir: rootOverride, }); return; } if (command === 'config') { await handleConfigCli({ loadOptions: { configPath, rootDir: rootOverride }, invokeAuth: (authArgs) => invokeAuthCommand(runtimeOptionsWithPath, authArgs), }, args); return; } if (command === 'emit-ts') { const runtime = await createRuntime(runtimeOptionsWithPath); try { await handleEmitTs(runtime, args); } finally { await runtime.close().catch(() => { }); } return; } const baseRuntime = await createRuntime(runtimeOptionsWithPath); const keepAliveServers = new Set(baseRuntime .getDefinitions() .filter(isKeepAliveServer) .map((entry) => entry.name)); const daemonClient = keepAliveServers.size > 0 ? new DaemonClient({ configPath: configResolution.path, configExplicit: configResolution.explicit, rootDir: rootOverride, }) : null; const runtime = createKeepAliveRuntime(baseRuntime, { daemonClient, keepAliveServers }); const inference = inferCommandRouting(command, args, runtime.getDefinitions()); if (inference.kind === 'abort') { process.exitCode = inference.exitCode; return; } const resolvedCommand = inference.command; const resolvedArgs = inference.args; try { if (resolvedCommand === 'list') { if (consumeHelpTokens(resolvedArgs)) { printListHelp(); process.exitCode = 0; return; } await handleList(runtime, resolvedArgs); return; } if (resolvedCommand === 'call') { if (consumeHelpTokens(resolvedArgs)) { printCallHelp(); process.exitCode = 0; return; } await runHandleCall(runtime, resolvedArgs); return; } if (resolvedCommand === 'auth') { if (consumeHelpTokens(resolvedArgs)) { printAuthHelp(); process.exitCode = 0; return; } await handleAuth(runtime, resolvedArgs); return; } } finally { const closeStart = Date.now(); if (DEBUG_HANG) { logInfo('[debug] beginning runtime.close()'); dumpActiveHandles('before runtime.close'); } try { await runtime.close(); if (DEBUG_HANG) { const duration = Date.now() - closeStart; logInfo(`[debug] runtime.close() completed in ${duration}ms`); dumpActiveHandles('after runtime.close'); } } catch (error) { if (DEBUG_HANG) { logError('[debug] runtime.close() failed', error); } } finally { terminateChildProcesses('runtime.finally'); // By default we force an exit after cleanup so Node doesn't hang on lingering stdio handles // (see typescript-sdk#579/#780/#1049). Opt out by exporting MCPORTER_NO_FORCE_EXIT=1. const disableForceExit = process.env.MCPORTER_NO_FORCE_EXIT === '1'; if (DEBUG_HANG) { dumpActiveHandles('after terminateChildProcesses'); if (!disableForceExit || process.env.MCPORTER_FORCE_EXIT === '1') { process.exit(0); } } else { const scheduleExit = () => { if (!disableForceExit || process.env.MCPORTER_FORCE_EXIT === '1') { process.exit(0); } }; setImmediate(scheduleExit); } } } printHelp(`Unknown command '${resolvedCommand}'.`); process.exit(1); } // main parses CLI flags and dispatches to list/call commands. async function main() { await runCli(process.argv.slice(2)); } // printHelp explains available commands and global flags. function printHelp(message) { if (message) { console.error(message); console.error(''); } const colorize = supportsAnsiColor; const sections = buildCommandSections(colorize); const globalFlags = formatGlobalFlags(colorize); const quickStart = formatQuickStart(colorize); const footer = formatHelpFooter(colorize); const title = colorize ? `${boldText('mcporter')} ${dimText('— Model Context Protocol CLI & generator')}` : 'mcporter — Model Context Protocol CLI & generator'; const lines = [ title, '', 'Usage: mcporter [options]', '', ...sections, '', globalFlags, '', quickStart, '', footer, ]; console.error(lines.join('\n')); } function buildCommandSections(colorize) { const sections = [ { title: 'Core commands', entries: [ { name: 'list', summary: 'List configured servers (add --schema for tool docs)', usage: 'mcporter list [name] [--schema] [--json]', }, { name: 'call', summary: 'Call a tool by selector (server.tool) or HTTP URL; key=value flags supported', usage: 'mcporter call [key=value ...]', }, { name: 'auth', summary: 'Complete OAuth for a server without listing tools', usage: 'mcporter auth [--reset]', }, ], }, { title: 'Generator & tooling', entries: [ { name: 'generate-cli', summary: 'Emit a standalone CLI (supports HTTP, stdio, and inline commands)', usage: 'mcporter generate-cli --server | --command [options]', }, { name: 'inspect-cli', summary: 'Show metadata and regen instructions for a generated CLI', usage: 'mcporter inspect-cli [--json]', }, { name: 'emit-ts', summary: 'Generate TypeScript client/types for a server', usage: 'mcporter emit-ts --mode client|types [options]', }, ], }, { title: 'Configuration', entries: [ { name: 'config', summary: 'Inspect or edit config files (list, get, add, remove, import, login, logout)', usage: 'mcporter config [options]', }, ], }, { title: 'Daemon', entries: [ { name: 'daemon', summary: 'Manage the keep-alive daemon (start | status | stop | restart)', usage: 'mcporter daemon ', }, ], }, ]; return sections.flatMap((section) => formatCommandSection(section, colorize)); } function formatCommandSection(section, colorize) { const maxNameLength = Math.max(...section.entries.map((entry) => entry.name.length)); const header = colorize ? boldText(section.title) : section.title; const lines = [header]; section.entries.forEach((entry) => { const paddedName = entry.name.padEnd(maxNameLength); const renderedName = colorize ? boldText(paddedName) : paddedName; const summary = colorize ? dimText(entry.summary) : entry.summary; lines.push(` ${renderedName} ${summary}`); lines.push(` ${extraDimText('usage:')} ${entry.usage}`); }); return [...lines, '']; } function formatGlobalFlags(colorize) { const title = colorize ? boldText('Global flags') : 'Global flags'; const entries = [ { flag: '--config ', summary: 'Path to mcporter.json (defaults to ./config/mcporter.json)', }, { flag: '--root ', summary: 'Working directory for stdio servers', }, { flag: '--log-level ', summary: 'Adjust CLI logging (defaults to warn)', }, { flag: '--oauth-timeout ', summary: 'Time to wait for browser-based OAuth before giving up (default 60000)', }, ]; const formatted = entries.map((entry) => ` ${entry.flag.padEnd(34)}${entry.summary}`); return [title, ...formatted].join('\n'); } function formatQuickStart(colorize) { const title = colorize ? boldText('Quick start') : 'Quick start'; const entries = [ ['mcporter list', 'show configured servers'], ['mcporter list linear --schema', 'view Linear tool docs'], ['mcporter call linear.list_issues limit:5', 'invoke a tool with key=value arguments'], ['mcporter generate-cli --command https://host/mcp --compile ./my-cli', 'build a standalone CLI/binary'], ]; const formatted = entries.map(([cmd, note]) => { const comment = colorize ? dimText(`# ${note}`) : `# ${note}`; return ` ${cmd}\n ${comment}`; }); return [title, ...formatted].join('\n'); } function formatHelpFooter(colorize) { const pointer = 'Run `mcporter --help` for detailed flags.'; const autoLoad = 'mcporter auto-loads servers from ./config/mcporter.json and editor imports (Cursor, Claude, Codex, etc.).'; if (!colorize) { return `${pointer}\n${autoLoad}`; } return `${dimText(pointer)}\n${extraDimText(autoLoad)}`; } async function printVersion() { console.log(await resolveCliVersion()); } function isHelpToken(token) { return token === '--help' || token === '-h' || token === 'help'; } function consumeHelpTokens(args) { let found = false; for (let index = args.length - 1; index >= 0; index -= 1) { const token = args[index]; if (token && isHelpToken(token)) { args.splice(index, 1); found = true; } } return found; } function isVersionToken(token) { return token === '--version' || token === '-v' || token === '-V'; } async function resolveCliVersion() { try { const packageJsonPath = new URL('../package.json', import.meta.url); const buffer = await fsPromises.readFile(packageJsonPath, 'utf8'); const pkg = JSON.parse(buffer); return pkg.version ?? MCPORTER_VERSION; } catch { return MCPORTER_VERSION; } } if (process.env.MCPORTER_DISABLE_AUTORUN !== '1') { main().catch((error) => { if (error instanceof CliUsageError) { logError(error.message); process.exit(1); return; } const message = error instanceof Error ? error.message : String(error); logError(message, error instanceof Error ? error : undefined); process.exit(1); }); } // handleAuth clears cached tokens and executes standalone OAuth flows. export async function handleAuth(runtime, args) { // Peel off optional flags before we consume positional args. const resetIndex = args.indexOf('--reset'); const shouldReset = resetIndex !== -1; if (shouldReset) { args.splice(resetIndex, 1); } const format = consumeOutputFormat(args, { defaultFormat: 'text', allowed: ['text', 'json'], enableRawShortcut: false, jsonShortcutFlag: '--json', }); const ephemeralSpec = extractEphemeralServerFlags(args); let target = args.shift(); const nameHints = []; if (ephemeralSpec && target && !looksLikeHttpUrl(target)) { nameHints.push(target); } const prepared = await prepareEphemeralServerTarget({ runtime, target, ephemeral: ephemeralSpec, nameHints, reuseFromSpec: true, }); target = prepared.target; if (!target) { throw new Error('Usage: mcporter auth [--http-url | --stdio ]'); } const definition = runtime.getDefinition(target); if (shouldReset) { await clearOAuthCaches(definition); logInfo(`Cleared cached credentials for '${target}'.`); } if (definition.command.kind === 'stdio' && definition.oauthCommand) { logInfo(`Starting auth helper for '${target}' (stdio). Leave this running until the browser flow completes.`); await runStdioAuth(definition); logInfo(`Auth helper for '${target}' finished. You can now call tools.`); return; } // Kick off the interactive OAuth flow without blocking list output. We retry once if the // server gets auto-promoted to OAuth mid-flight. for (let attempt = 0; attempt < 2; attempt += 1) { try { logInfo(`Initiating OAuth flow for '${target}'...`); const tools = await runtime.listTools(target, { autoAuthorize: true }); logInfo(`Authorization complete. ${tools.length} tool${tools.length === 1 ? '' : 's'} available.`); return; } catch (error) { if (attempt === 0 && shouldRetryAuthError(error)) { logWarn('Server signaled OAuth after the initial attempt. Retrying with browser flow...'); continue; } const message = error instanceof Error ? error.message : String(error); if (format === 'json') { const payload = buildConnectionIssueEnvelope({ server: target, error, issue: analyzeConnectionError(error), }); console.log(JSON.stringify(payload, null, 2)); process.exitCode = 1; return; } throw new Error(`Failed to authorize '${target}': ${message}`); } } } async function invokeAuthCommand(runtimeOptions, args) { const runtime = await createRuntime(runtimeOptions); try { await handleAuth(runtime, args); } finally { await runtime.close().catch(() => { }); } } async function runStdioAuth(definition) { const authArgs = [...(definition.command.kind === 'stdio' ? (definition.command.args ?? []) : [])]; if (definition.oauthCommand) { authArgs.push(...definition.oauthCommand.args); } return new Promise((resolve, reject) => { const child = spawn(definition.command.kind === 'stdio' ? definition.command.command : '', authArgs, { stdio: 'inherit', cwd: definition.command.kind === 'stdio' ? definition.command.cwd : process.cwd(), env: process.env, }); child.on('error', reject); child.on('exit', (code) => { if (code === 0) { resolve(); } else { reject(new Error(`Auth helper exited with code ${code ?? 'null'}`)); } }); }); } function shouldRetryAuthError(error) { return analyzeConnectionError(error).kind === 'auth'; } export function printAuthHelp() { const lines = [ 'Usage: mcporter auth [flags]', '', 'Purpose:', ' Run the authentication flow for a server without listing tools.', '', 'Common flags:', ' --reset Clear cached credentials before re-authorizing.', ' --json Emit a JSON envelope on failure.', '', 'Ad-hoc targets:', ' --http-url Register an HTTP server for this run.', ' --allow-http Permit plain http:// URLs with --http-url.', ' --stdio Run a stdio MCP server (repeat --stdio-arg for args).', ' --stdio-arg Append args to the stdio command (repeatable).', ' --env KEY=value Inject env vars for stdio servers (repeatable).', ' --cwd Working directory for stdio servers.', ' --name Override the display name for ad-hoc servers.', ' --description Override the description for ad-hoc servers.', ' --persist Write the ad-hoc definition to config/mcporter.json.', ' --yes Skip confirmation prompts when persisting.', '', 'Examples:', ' mcporter auth linear', ' mcporter auth https://mcp.example.com/mcp', ' mcporter auth --stdio "npx -y chrome-devtools-mcp@latest"', ' mcporter auth --http-url http://localhost:3000/mcp --allow-http', ]; console.error(lines.join('\n')); } //# sourceMappingURL=cli.js.map