(mcp: Command)
| 22 | import { updateSettingsForSource } from '../../utils/settings/settings.js' |
| 23 | |
| 24 | export function registerMcpXaaIdpCommand(mcp: Command): void { |
| 25 | const xaaIdp = mcp |
| 26 | .command('xaa') |
| 27 | .description('Manage the XAA (SEP-990) IdP connection') |
| 28 | |
| 29 | xaaIdp |
| 30 | .command('setup') |
| 31 | .description( |
| 32 | 'Configure the IdP connection (one-time setup for all XAA-enabled servers)', |
| 33 | ) |
| 34 | .requiredOption('--issuer <url>', 'IdP issuer URL (OIDC discovery)') |
| 35 | .requiredOption('--client-id <id>', "Claude Code's client_id at the IdP") |
| 36 | .option( |
| 37 | '--client-secret', |
| 38 | 'Read IdP client secret from MCP_XAA_IDP_CLIENT_SECRET env var', |
| 39 | ) |
| 40 | .option( |
| 41 | '--callback-port <port>', |
| 42 | 'Fixed loopback callback port (only if IdP does not honor RFC 8252 port-any matching)', |
| 43 | ) |
| 44 | .action(options => { |
| 45 | // Validate everything BEFORE any writes. An exit(1) mid-write leaves |
| 46 | // settings configured but keychain missing — confusing state. |
| 47 | // updateSettingsForSource doesn't schema-check on write; a non-URL |
| 48 | // issuer lands on disk and then poisons the whole userSettings source |
| 49 | // on next launch (SettingsSchema .url() fails → parseSettingsFile |
| 50 | // returns { settings: null }, dropping everything, not just xaaIdp). |
| 51 | let issuerUrl: URL |
| 52 | try { |
| 53 | issuerUrl = new URL(options.issuer) |
| 54 | } catch { |
| 55 | return cliError( |
| 56 | `Error: --issuer must be a valid URL (got "${options.issuer}")`, |
| 57 | ) |
| 58 | } |
| 59 | // OIDC discovery + token exchange run against this host. Allow http:// |
| 60 | // only for loopback (conformance harness mock IdP); anything else leaks |
| 61 | // the client secret and authorization code over plaintext. |
| 62 | if ( |
| 63 | issuerUrl.protocol !== 'https:' && |
| 64 | !( |
| 65 | issuerUrl.protocol === 'http:' && |
| 66 | (issuerUrl.hostname === 'localhost' || |
| 67 | issuerUrl.hostname === '127.0.0.1' || |
| 68 | issuerUrl.hostname === '[::1]') |
| 69 | ) |
| 70 | ) { |
| 71 | return cliError( |
| 72 | `Error: --issuer must use https:// (got "${issuerUrl.protocol}//${issuerUrl.host}")`, |
| 73 | ) |
| 74 | } |
| 75 | const callbackPort = options.callbackPort |
| 76 | ? parseInt(options.callbackPort, 10) |
| 77 | : undefined |
| 78 | // callbackPort <= 0 fails Zod's .positive() on next launch — same |
| 79 | // settings-poisoning failure mode as the issuer check above. |
| 80 | if ( |
| 81 | callbackPort !== undefined && |
no test coverage detected