import { readClientPolicyBlock } from "./GeminiClientSettings.js"; import { effectiveClientConfiguration, readClientConfigurationFiles, } from "./ClientConfigurationFiles.js"; import { clientServerForcedEnabled, clientServerListedDisabled, effectiveClientServer, } from "./ClientConfigurationDocument.js"; import { access } from "node:fs/promises"; import { resolve } from "node:path"; import { z } from "zod"; import { PRODUCT_IDENTITY } from "../identity.js"; import { MCP_STARTUP_POLICY } from "../mcpStartupPolicy.js"; import { isOwnedClientRegistrationCommand, npxRegistrationCommand, } from "./ClientRegistrationIdentity.js"; import { clientEvidencePaths, manualRegistrationRemediation, supportedClients, } from "./SupportedClients.js"; import type { SetupClient } from "./SupportedClients.js"; interface ClientRegistrationStatusBase { readonly client: string; readonly config_path: string; } type RegistrationCommand = readonly [string, ...string[]]; export type ClientRegistrationStatus = ClientRegistrationStatusBase & ( | { readonly command: RegistrationCommand; readonly state: "aligned"; readonly remediation: null; } | { readonly command: RegistrationCommand; readonly state: "stale"; readonly remediation: string; } | { readonly command: readonly []; readonly state: "missing" | "invalid" | "manual"; readonly remediation: string; } ); export type UnhealthyClientRegistrationStatus = Exclude< ClientRegistrationStatus, { readonly state: "aligned" } >; const registrationSchema = z .object({ command: z.string().min(1), args: z.array(z.string()).default([]), startup_timeout_sec: z.number().positive().optional(), type: z.string().optional(), transport: z.string().optional(), tools: z.array(z.string()).optional(), disabled: z.boolean().optional(), enabled: z.boolean().optional(), }) .passthrough(); /** Inspect supported client registrations without reading their environment. */ export const readClientRegistrationStatuses = async ( home: string, currentCommandPath: string = resolve(process.argv[1] ?? "unknown"), options: { readonly platform?: NodeJS.Platform; readonly environment?: NodeJS.ProcessEnv; } = {}, ): Promise => { const statuses: ClientRegistrationStatus[] = []; for (const client of supportedClients( home, options.platform, options.environment, )) { if (client.configPathError !== undefined) { statuses.push({ client: client.name, config_path: client.configPath, command: [], state: "invalid", remediation: client.configPathError, }); continue; } if (!(await clientEvidencePresent(client))) continue; const manualRemediation = manualRegistrationRemediation(client.name); if (client.format === "unsupported") { if (manualRemediation !== undefined) statuses.push({ client: client.name, config_path: client.markerPath ?? client.configPath, command: [], state: "manual", remediation: manualRemediation, }); continue; } try { const files = await readClientConfigurationFiles(client); if (!files.ok) { statuses.push({ client: client.name, config_path: client.configPath, command: [], state: "invalid", remediation: files.error.detail, }); continue; } const parsed = effectiveClientConfiguration(files.value); if (parsed === undefined) { statuses.push( unavailableStatus(client.name, client.configPath, "missing"), ); continue; } const policy = await readClientPolicyBlock(client, parsed); if (!policy.ok) { statuses.push({ client: client.name, config_path: client.configPath, command: [], state: "invalid", remediation: policy.error.detail, }); continue; } const policyBlock = policy.value; const raw = effectiveClientServer(parsed, PRODUCT_IDENTITY.mcpServerKey); if (raw === undefined) { statuses.push( unavailableStatus(client.name, client.configPath, "missing"), ); continue; } const registration = parseRegistration(raw, client); const command: RegistrationCommand = [ registration.command, ...registration.args, ]; statuses.push( configuredStatus( client.name, client.configPath, command, registrationAligned( registration, client, currentCommandPath, options.platform ?? process.platform, clientServerForcedEnabled(parsed, PRODUCT_IDENTITY.mcpServerKey), ) && !clientServerListedDisabled( parsed, PRODUCT_IDENTITY.mcpServerKey, ) && policyBlock === undefined ? "aligned" : "stale", policyBlock, ), ); } catch (cause: unknown) { statuses.push( unavailableStatus( client.name, client.configPath, isMissing(cause) ? "missing" : "invalid", ), ); } } return statuses.sort((left, right) => left.client.localeCompare(right.client), ); }; const registrationAligned = ( registration: z.output, client: SetupClient, currentCommandPath: string, platform: NodeJS.Platform, forcedEnabled = false, ): boolean => { const command = [registration.command, ...registration.args]; if ( (client.format !== "grok" && registration.disabled === true) || (registration.enabled === false && !forcedEnabled) ) return false; if (!isOwnedClientRegistrationCommand(command, currentCommandPath)) return false; if ( (client.name === "codex" || client.name === "grok_build") && registration.startup_timeout_sec !== MCP_STARTUP_POLICY.codexStartupTimeoutSeconds ) return false; if (client.format === "vscode" && registration.type !== "stdio") return false; // OMP and Pi infer stdio for a command entry without an explicit type. if ( (client.format === "omp" || client.format === "pi") && registration.type !== undefined && registration.type !== "stdio" ) return false; if ( client.format === "copilot_cli" && (registration.type !== "stdio" || JSON.stringify(registration.tools) !== JSON.stringify(["*"])) ) return false; if (client.format === "commandcode" && registration.transport !== "stdio") return false; if ( command.length === 3 && command[2] === "mcp" && resolve(command[0] ?? "") === resolve(process.execPath) && resolve(command[1] ?? "") === currentCommandPath ) return true; const npxCommand = npxRegistrationCommand(platform); if ( command.length === npxCommand.length && command.every((argument, index) => argument === npxCommand[index]) ) return true; return ( command.length === 2 && command[1] === "mcp" && resolve(command[0] ?? "") === currentCommandPath ); }; const parseRegistration = ( value: unknown, client: SetupClient, ): z.output => { if (client.format === "opencode") { const entry = z .object({ type: z.literal("local"), command: z.array(z.string()).min(1), environment: z.record(z.string(), z.string()).optional(), }) .passthrough() .parse(value); const [command = "", ...args] = entry.command; return registrationSchema.parse({ ...entry, command, args, }); } const registration = registrationSchema.parse(value); if ( (client.format === "vscode" || client.format === "copilot_cli") && registration.type !== "stdio" ) throw new TypeError("Expected an stdio registration"); if (client.format === "commandcode" && registration.transport !== "stdio") throw new TypeError("Expected an stdio registration"); if ( (client.format === "omp" || client.format === "pi") && registration.type !== undefined && registration.type !== "stdio" ) throw new TypeError("Expected an stdio registration"); return registration; }; const remediation = "Run rea setup to refresh this registration, then restart the client."; const configuredStatus = ( client: string, configPath: string, command: RegistrationCommand, state: "aligned" | "stale", policyRemediation?: string, ): ClientRegistrationStatus => state === "aligned" ? { client, config_path: configPath, command, state, remediation: null } : { client, config_path: configPath, command, state, remediation: policyRemediation ?? remediation, }; const unavailableStatus = ( client: string, configPath: string, state: "missing" | "invalid", ): ClientRegistrationStatus => ({ client, config_path: configPath, command: [], state, remediation, }); const clientEvidencePresent = async (client: SetupClient): Promise => { for (const path of clientEvidencePaths(client)) if (await exists(path)) return true; return false; }; const exists = async (path: string | undefined): Promise => { if (path === undefined) return false; try { await access(path); return true; } catch (cause: unknown) { // best-effort cleanup: optional registration probing; absence means unregistered. void cause; return false; } }; const isMissing = (cause: unknown): boolean => cause instanceof Error && "code" in cause && cause.code === "ENOENT";