| |
| |
| |
| |
| |
| import { type Command, Option } from '@commander-js/extra-typings' |
| import { cliError, cliOk } from '../../cli/exit.js' |
| import { |
| type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| logEvent, |
| } from '../../services/analytics/index.js' |
| import { |
| readClientSecret, |
| saveMcpClientSecret, |
| } from '../../services/mcp/auth.js' |
| import { addMcpConfig } from '../../services/mcp/config.js' |
| import { |
| describeMcpConfigFilePath, |
| ensureConfigScope, |
| ensureTransport, |
| parseHeaders, |
| } from '../../services/mcp/utils.js' |
| import { |
| getXaaIdpSettings, |
| isXaaEnabled, |
| } from '../../services/mcp/xaaIdpLogin.js' |
| import { parseEnvVars } from '../../utils/envUtils.js' |
| import { jsonStringify } from '../../utils/slowOperations.js' |
|
|
| |
| |
| |
| export function registerMcpAddCommand(mcp: Command): void { |
| mcp |
| .command('add <name> <commandOrUrl> [args...]') |
| .description( |
| 'Add an MCP server to Claude Code.\n\n' + |
| 'Examples:\n' + |
| ' # Add HTTP server:\n' + |
| ' claude mcp add --transport http sentry https://mcp.sentry.dev/mcp\n\n' + |
| ' # Add HTTP server with headers:\n' + |
| ' claude mcp add --transport http corridor https://app.corridor.dev/api/mcp --header "Authorization: Bearer ..."\n\n' + |
| ' # Add stdio server with environment variables:\n' + |
| ' claude mcp add -e API_KEY=xxx my-server -- npx my-mcp-server\n\n' + |
| ' # Add stdio server with subprocess flags:\n' + |
| ' claude mcp add my-server -- my-command --some-flag arg1', |
| ) |
| .option( |
| '-s, --scope <scope>', |
| 'Configuration scope (local, user, or project)', |
| 'local', |
| ) |
| .option( |
| '-t, --transport <transport>', |
| 'Transport type (stdio, sse, http). Defaults to stdio if not specified.', |
| ) |
| .option( |
| '-e, --env <env...>', |
| 'Set environment variables (e.g. -e KEY=value)', |
| ) |
| .option( |
| '-H, --header <header...>', |
| 'Set WebSocket headers (e.g. -H "X-Api-Key: abc123" -H "X-Custom: value")', |
| ) |
| .option('--client-id <clientId>', 'OAuth client ID for HTTP/SSE servers') |
| .option( |
| '--client-secret', |
| 'Prompt for OAuth client secret (or set MCP_CLIENT_SECRET env var)', |
| ) |
| .option( |
| '--callback-port <port>', |
| 'Fixed port for OAuth callback (for servers requiring pre-registered redirect URIs)', |
| ) |
| .helpOption('-h, --help', 'Display help for command') |
| .addOption( |
| new Option( |
| '--xaa', |
| "Enable XAA (SEP-990) for this server. Requires 'claude mcp xaa setup' first. Also requires --client-id and --client-secret (for the MCP server's AS).", |
| ).hideHelp(!isXaaEnabled()), |
| ) |
| .action(async (name, commandOrUrl, args, options) => { |
| |
| const actualCommand = commandOrUrl |
| const actualArgs = args |
|
|
| |
| if (!name) { |
| cliError( |
| 'Error: Server name is required.\n' + |
| 'Usage: claude mcp add <name> <command> [args...]', |
| ) |
| } else if (!actualCommand) { |
| cliError( |
| 'Error: Command is required when server name is provided.\n' + |
| 'Usage: claude mcp add <name> <command> [args...]', |
| ) |
| } |
|
|
| try { |
| const scope = ensureConfigScope(options.scope) |
| const transport = ensureTransport(options.transport) |
|
|
| |
| if (options.xaa && !isXaaEnabled()) { |
| cliError( |
| 'Error: --xaa requires CLAUDE_CODE_ENABLE_XAA=1 in your environment', |
| ) |
| } |
| const xaa = Boolean(options.xaa) |
| if (xaa) { |
| const missing: string[] = [] |
| if (!options.clientId) missing.push('--client-id') |
| if (!options.clientSecret) missing.push('--client-secret') |
| if (!getXaaIdpSettings()) { |
| missing.push( |
| "'claude mcp xaa setup' (settings.xaaIdp not configured)", |
| ) |
| } |
| if (missing.length) { |
| cliError(`Error: --xaa requires: ${missing.join(', ')}`) |
| } |
| } |
|
|
| |
| const transportExplicit = options.transport !== undefined |
|
|
| |
| const looksLikeUrl = |
| actualCommand.startsWith('http://') || |
| actualCommand.startsWith('https://') || |
| actualCommand.startsWith('localhost') || |
| actualCommand.endsWith('/sse') || |
| actualCommand.endsWith('/mcp') |
|
|
| logEvent('tengu_mcp_add', { |
| type: transport as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| scope: |
| scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| source: |
| 'command' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| transport: |
| transport as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| transportExplicit: transportExplicit, |
| looksLikeUrl: looksLikeUrl, |
| }) |
|
|
| if (transport === 'sse') { |
| if (!actualCommand) { |
| cliError('Error: URL is required for SSE transport.') |
| } |
|
|
| const headers = options.header |
| ? parseHeaders(options.header) |
| : undefined |
|
|
| const callbackPort = options.callbackPort |
| ? parseInt(options.callbackPort, 10) |
| : undefined |
| const oauth = |
| options.clientId || callbackPort || xaa |
| ? { |
| ...(options.clientId ? { clientId: options.clientId } : {}), |
| ...(callbackPort ? { callbackPort } : {}), |
| ...(xaa ? { xaa: true } : {}), |
| } |
| : undefined |
|
|
| const clientSecret = |
| options.clientSecret && options.clientId |
| ? await readClientSecret() |
| : undefined |
|
|
| const serverConfig = { |
| type: 'sse' as const, |
| url: actualCommand, |
| headers, |
| oauth, |
| } |
| await addMcpConfig(name, serverConfig, scope) |
|
|
| if (clientSecret) { |
| saveMcpClientSecret(name, serverConfig, clientSecret) |
| } |
|
|
| process.stdout.write( |
| `Added SSE MCP server ${name} with URL: ${actualCommand} to ${scope} config\n`, |
| ) |
| if (headers) { |
| process.stdout.write( |
| `Headers: ${jsonStringify(headers, null, 2)}\n`, |
| ) |
| } |
| } else if (transport === 'http') { |
| if (!actualCommand) { |
| cliError('Error: URL is required for HTTP transport.') |
| } |
|
|
| const headers = options.header |
| ? parseHeaders(options.header) |
| : undefined |
|
|
| const callbackPort = options.callbackPort |
| ? parseInt(options.callbackPort, 10) |
| : undefined |
| const oauth = |
| options.clientId || callbackPort || xaa |
| ? { |
| ...(options.clientId ? { clientId: options.clientId } : {}), |
| ...(callbackPort ? { callbackPort } : {}), |
| ...(xaa ? { xaa: true } : {}), |
| } |
| : undefined |
|
|
| const clientSecret = |
| options.clientSecret && options.clientId |
| ? await readClientSecret() |
| : undefined |
|
|
| const serverConfig = { |
| type: 'http' as const, |
| url: actualCommand, |
| headers, |
| oauth, |
| } |
| await addMcpConfig(name, serverConfig, scope) |
|
|
| if (clientSecret) { |
| saveMcpClientSecret(name, serverConfig, clientSecret) |
| } |
|
|
| process.stdout.write( |
| `Added HTTP MCP server ${name} with URL: ${actualCommand} to ${scope} config\n`, |
| ) |
| if (headers) { |
| process.stdout.write( |
| `Headers: ${jsonStringify(headers, null, 2)}\n`, |
| ) |
| } |
| } else { |
| if ( |
| options.clientId || |
| options.clientSecret || |
| options.callbackPort || |
| options.xaa |
| ) { |
| process.stderr.write( |
| `Warning: --client-id, --client-secret, --callback-port, and --xaa are only supported for HTTP/SSE transports and will be ignored for stdio.\n`, |
| ) |
| } |
|
|
| |
| if (!transportExplicit && looksLikeUrl) { |
| process.stderr.write( |
| `\nWarning: The command "${actualCommand}" looks like a URL, but is being interpreted as a stdio server as --transport was not specified.\n`, |
| ) |
| process.stderr.write( |
| `If this is an HTTP server, use: claude mcp add --transport http ${name} ${actualCommand}\n`, |
| ) |
| process.stderr.write( |
| `If this is an SSE server, use: claude mcp add --transport sse ${name} ${actualCommand}\n`, |
| ) |
| } |
|
|
| const env = parseEnvVars(options.env) |
| await addMcpConfig( |
| name, |
| { type: 'stdio', command: actualCommand, args: actualArgs, env }, |
| scope, |
| ) |
|
|
| process.stdout.write( |
| `Added stdio MCP server ${name} with command: ${actualCommand} ${actualArgs.join(' ')} to ${scope} config\n`, |
| ) |
| } |
| cliOk(`File modified: ${describeMcpConfigFilePath(scope)}`) |
| } catch (error) { |
| cliError((error as Error).message) |
| } |
| }) |
| } |
|
|