From 69d8afefc19e014a4bba7a182983a9e77e100a75 Mon Sep 17 00:00:00 2001 From: Iwakurakaoru0822 Date: Tue, 6 Oct 2026 21:58:20 -0700 Subject: [PATCH] fix(setup): register OpenCode V2 configurations in mcp.servers OpenCode V2 reads native server entries from mcp.servers, and a native entry takes precedence over a V1 mcp. entry. After OpenCode migrated a configuration, setup wrote an ignored V1 entry, status reported REA as missing, and uninstall left the native entry in place. Co-Authored-By: Claude Opus 5.5 --- docs/installation.md | 5 + .../ClientConfigurationDocument.test.ts | 16 +- .../ClientConfigurationDocument.ts | 138 +++++++++++++--- src/application/ClientRegistrationStatus.ts | 11 +- src/application/SetupClientConfiguration.ts | 99 ++++++------ src/application/Uninstall.ts | 50 ++++-- src/application/UpdateMaintenance.ts | 9 +- .../clientConfigurationDialects.test.ts | 151 ++++++++++++++++++ 8 files changed, 386 insertions(+), 93 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index acb0036bd..231d7c22d 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -131,6 +131,11 @@ Setup can configure these clients for REA's local MCP server: | Command Code | `commandcode` | | VS Code | `vscode` | +For OpenCode, setup writes the V1 `mcp.rea` entry, which OpenCode V1 and V2 +both load. If the configuration already uses OpenCode V2's native +`mcp.servers` table, setup registers REA there instead and replaces any earlier +`mcp.rea` entry from REA. + ## Review setup changes `rea setup` first offers the supported agents in a multi-select. Existing REA diff --git a/src/application/ClientConfigurationDocument.test.ts b/src/application/ClientConfigurationDocument.test.ts index 88958895d..1625e499c 100644 --- a/src/application/ClientConfigurationDocument.test.ts +++ b/src/application/ClientConfigurationDocument.test.ts @@ -22,7 +22,13 @@ describe.each([ expect( parseClientConfiguration(`${bom}${JSON.stringify(document)}`, format), - ).toEqual({ document, servers: document[serversKey], serversKey }); + ).toEqual({ + document, + servers: document[serversKey], + serversPath: [serversKey], + dialect: format, + legacyServers: {}, + }); }); it.each(["", "\uFEFF"])( @@ -70,12 +76,16 @@ describe.each([ else servers.rea = registration; const document = { ...parsed.document, [serversKey]: servers }; - text = serializeClientConfiguration(document, format, text, "rea"); + text = serializeClientConfiguration(document, format, text, [ + [serversKey, "rea"], + ]); expect(parseClientConfiguration(text, format)).toEqual({ document, servers, - serversKey, + serversPath: [serversKey], + dialect: format, + legacyServers: {}, }); expect(Buffer.from(text.slice(0, prefix.length))).toEqual( Buffer.from(prefix), diff --git a/src/application/ClientConfigurationDocument.ts b/src/application/ClientConfigurationDocument.ts index 6eb2da3a8..9af15be08 100644 --- a/src/application/ClientConfigurationDocument.ts +++ b/src/application/ClientConfigurationDocument.ts @@ -14,11 +14,27 @@ import type { SetupClient } from "./SupportedClients.js"; export type ClientConfigurationFormat = NonNullable; export type ClientServersKey = "mcp_servers" | "mcpServers" | "mcp" | "servers"; +/** + * Registration entry dialect. OpenCode V2 reads V1 `mcp.` entries, but a + * document already using the native `mcp.servers` table takes native entries. + */ +export type ClientRegistrationDialect = + | ClientConfigurationFormat + | "opencode_v2"; + /** Validated client document and its server table, preserving unrelated settings. */ export interface ClientConfigurationDocument { readonly document: Record; + /** Server table that registrations are written to. */ readonly servers: Record; - readonly serversKey: ClientServersKey; + /** Key path of `servers` in `document`. */ + readonly serversPath: readonly string[]; + readonly dialect: ClientRegistrationDialect; + /** + * OpenCode V1 server members beside a native V2 `mcp.servers` table. OpenCode + * still loads them, and a native entry with the same name takes precedence. + */ + readonly legacyServers: Record; } const objectSchema = z.record(z.string(), z.unknown()); @@ -77,6 +93,9 @@ const parseDocument = ( return objectSchema.parse(document); }; +/** Members of OpenCode V2's `mcp` object that are not server names. */ +const OPENCODE_V2_MCP_SETTINGS = new Set(["servers", "timeout"]); + /** Parse a client document and reject malformed roots or MCP server tables. */ export const parseClientConfiguration = ( text: string, @@ -88,7 +107,71 @@ export const parseClientConfiguration = ( const serversKey = clientConfigurationServersKey(format); const value = document[serversKey]; const servers = value === undefined ? {} : objectSchema.parse(value); - return { document, servers, serversKey }; + const native = servers.servers; + // A V1 server named `servers` is an entry with a `type`; the V2 table is not. + if ( + format !== "opencode" || + native === undefined || + (typeof native === "object" && native !== null && "type" in native) + ) + return { + document, + servers, + serversPath: [serversKey], + dialect: format, + legacyServers: {}, + }; + return { + document, + servers: objectSchema.parse(native), + serversPath: [serversKey, "servers"], + dialect: "opencode_v2", + legacyServers: Object.fromEntries( + Object.entries(servers).filter( + ([name]) => !OPENCODE_V2_MCP_SETTINGS.has(name), + ), + ), + }; +}; + +/** The registration a client loads for `name`, including OpenCode V1 entries. */ +export const effectiveClientServer = ( + parsed: ClientConfigurationDocument, + name: string, +): unknown => + Object.hasOwn(parsed.servers, name) + ? parsed.servers[name] + : parsed.legacyServers[name]; + +/** + * Return `parsed.document` with `servers` at its server table. A legacy OpenCode + * V1 member named in `removeLegacy` is removed beside a native V2 table. + */ +export const withClientServers = ( + parsed: ClientConfigurationDocument, + servers: Record, + removeLegacy?: string, +): Record => { + const [key = "", nested] = parsed.serversPath; + if (nested === undefined) return { ...parsed.document, [key]: servers }; + const outer = { ...objectSchema.parse(parsed.document[key]) }; + if (removeLegacy !== undefined && !OPENCODE_V2_MCP_SETTINGS.has(removeLegacy)) + delete outer[removeLegacy]; + return { ...parsed.document, [key]: { ...outer, [nested]: servers } }; +}; + +/** Read the value at a key path, or `undefined` when any member is absent. */ +const valueAt = ( + document: Record, + path: readonly string[], +): unknown => { + let current: unknown = document; + for (const key of path) { + const object = objectSchema.safeParse(current); + if (!object.success || !Object.hasOwn(object.data, key)) return undefined; + current = object.data[key]; + } + return current; }; /** Serialize a validated client document, retaining JSONC comments. */ @@ -96,33 +179,43 @@ export const serializeClientConfiguration = ( document: Record, format: ClientConfigurationFormat | undefined, originalText?: string, - editedServerName?: string, + editedPaths?: readonly (readonly string[])[], ): string => { if (format === undefined || format === "unsupported") throw new TypeError("client does not have a supported MCP config format"); if (format === "toml") return stringifyToml(document); - if (originalText !== undefined && editedServerName !== undefined) { - const serversKey = clientConfigurationServersKey(format); - const servers = objectSchema.parse(document[serversKey] ?? {}); - const editedValue = Object.hasOwn(servers, editedServerName) - ? servers[editedServerName] - : undefined; - const edits = modify( + if (originalText !== undefined && editedPaths !== undefined) + return editedPaths.reduce( + (text, path) => + applyEdits( + text, + modify(text, [...path], valueAt(document, path), { + formattingOptions: { insertSpaces: true, tabSize: 2, eol: "\n" }, + }), + ), originalText, - [serversKey, editedServerName], - editedValue, - { - formattingOptions: { insertSpaces: true, tabSize: 2, eol: "\n" }, - }, ); - return applyEdits(originalText, edits); - } return `${JSON.stringify(document, null, 2)}\n`; }; +/** Key path of a named registration in the server table. */ +export const clientServerPath = ( + parsed: ClientConfigurationDocument, + name: string, +): string[] => [...parsed.serversPath, name]; + +/** Key path of a present OpenCode V1 entry beside a native V2 table. */ +export const legacyClientServerPath = ( + parsed: ClientConfigurationDocument, + name: string, +): string[] | undefined => + Object.hasOwn(parsed.legacyServers, name) + ? [parsed.serversPath[0] ?? "mcp", name] + : undefined; + /** Build the stdio entry shape expected by one client's configuration dialect. */ export const clientRegistrationEntry = ( - format: ClientConfigurationFormat, + format: ClientRegistrationDialect, command: readonly string[], environment: Readonly>, ): Record => { @@ -137,6 +230,15 @@ export const clientRegistrationEntry = ( ? {} : { environment: { ...environment } }), }; + case "opencode_v2": + // V2 connects servers unless `disabled` is true. + return { + type: "local", + command: [...command], + ...(Object.keys(environment).length === 0 + ? {} + : { environment: { ...environment } }), + }; case "vscode": return { type: "stdio", diff --git a/src/application/ClientRegistrationStatus.ts b/src/application/ClientRegistrationStatus.ts index 7c3b9dbf2..3a820aa8e 100644 --- a/src/application/ClientRegistrationStatus.ts +++ b/src/application/ClientRegistrationStatus.ts @@ -1,4 +1,7 @@ -import { parseClientConfiguration } from "./ClientConfigurationDocument.js"; +import { + effectiveClientServer, + parseClientConfiguration, +} from "./ClientConfigurationDocument.js"; import { access, readFile } from "node:fs/promises"; import { resolve } from "node:path"; @@ -85,8 +88,10 @@ export const readClientRegistrationStatuses = async ( continue; try { const content = await readFile(client.configPath, "utf8"); - const { servers } = parseClientConfiguration(content, client.format); - const raw = servers[PRODUCT_IDENTITY.mcpServerKey]; + const raw = effectiveClientServer( + parseClientConfiguration(content, client.format), + PRODUCT_IDENTITY.mcpServerKey, + ); if (raw === undefined) { statuses.push( unavailableStatus(client.name, client.configPath, "missing"), diff --git a/src/application/SetupClientConfiguration.ts b/src/application/SetupClientConfiguration.ts index 04eace6b9..a9a231d99 100644 --- a/src/application/SetupClientConfiguration.ts +++ b/src/application/SetupClientConfiguration.ts @@ -1,9 +1,13 @@ import { clientRegistrationEntry, clientConfigurationValuesEqual, + clientServerPath, + legacyClientServerPath, parseClientConfiguration, serializeClientConfiguration, + withClientServers, type ClientConfigurationDocument, + type ClientRegistrationDialect, } from "./ClientConfigurationDocument.js"; import { constants as fsConstants } from "node:fs"; import { copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises"; @@ -82,20 +86,13 @@ const configureClientDocument = async ( void cause; return { status: "failed", reason: "readback" }; } - const { document, servers, serversKey } = parsed; const desired = clientConfigurationDesired( client, environment, command, - format, + parsed.dialect, ); - if ( - clientConfigurationValuesEqual( - servers[PRODUCT_IDENTITY.mcpServerKey], - desired, - ) - ) - return { status: "unchanged" }; + if (registrationCurrent(parsed, desired)) return { status: "unchanged" }; const backupPath = original === undefined ? undefined : `${client.configPath}.rea.backup`; if ( @@ -103,20 +100,25 @@ const configureClientDocument = async ( !(await preserveConfigBackup(transactionPath, backupPath)) ) return { status: "failed", reason: "backup" }; - document[serversKey] = { - ...servers, - [PRODUCT_IDENTITY.mcpServerKey]: desired, - }; + // A native OpenCode V2 entry replaces REA's legacy V1 entry, which would + // otherwise conflict with it. + const legacyPath = legacyClientServerPath( + parsed, + PRODUCT_IDENTITY.mcpServerKey, + ); + const document = withClientServers( + parsed, + { ...parsed.servers, [PRODUCT_IDENTITY.mcpServerKey]: desired }, + PRODUCT_IDENTITY.mcpServerKey, + ); try { await mkdir(dirname(client.configPath), { recursive: true }); await writeFileAtomic( transactionPath, - serializeClientConfiguration( - document, - format, - original, - PRODUCT_IDENTITY.mcpServerKey, - ), + serializeClientConfiguration(document, format, original, [ + clientServerPath(parsed, PRODUCT_IDENTITY.mcpServerKey), + ...(legacyPath === undefined ? [] : [legacyPath]), + ]), { encoding: "utf8", mode: 0o600, @@ -132,12 +134,7 @@ const configureClientDocument = async ( await readFile(transactionPath, "utf8"), format, ); - if ( - !clientConfigurationValuesEqual( - readback.servers[PRODUCT_IDENTITY.mcpServerKey], - desired, - ) - ) { + if (!registrationCurrent(readback, desired)) { await restoreConfig(transactionPath, original); return { status: "failed", reason: "readback" }; } @@ -159,18 +156,17 @@ export const clientConfigurationAligned = async ( providerEnvironment: SetupProviderEnvironment, command: readonly string[], ): Promise => { - const desired = clientConfigurationDesired( - client, - providerEnvironment, - command, - client.format, - ); try { const original = await readFile(client.configPath, "utf8"); - const { servers } = parseClientConfiguration(original, client.format); - return clientConfigurationValuesEqual( - servers[PRODUCT_IDENTITY.mcpServerKey], - desired, + const parsed = parseClientConfiguration(original, client.format); + return registrationCurrent( + parsed, + clientConfigurationDesired( + client, + providerEnvironment, + command, + parsed.dialect, + ), ); } catch (cause: unknown) { // Unreadable configuration is treated as not aligned so setup repairs it. @@ -206,20 +202,15 @@ export const inspectClientConfiguration = async ( "The configuration file could not be read. Check its permissions before rerunning setup.", }; } - const desired = clientConfigurationDesired( - client, - providerEnvironment, - command, - client.format, - ); try { - const { servers } = parseClientConfiguration(original, client.format); - if ( - clientConfigurationValuesEqual( - servers[PRODUCT_IDENTITY.mcpServerKey], - desired, - ) - ) + const parsed = parseClientConfiguration(original, client.format); + const desired = clientConfigurationDesired( + client, + providerEnvironment, + command, + parsed.dialect, + ); + if (registrationCurrent(parsed, desired)) return { status: "already_current" }; } catch (cause: unknown) { // Malformed configuration is reported with the invalid remediation. @@ -263,11 +254,21 @@ const restoreConfig = async ( } }; +/** Whether REA's entry matches and no conflicting legacy entry remains. */ +const registrationCurrent = ( + parsed: ClientConfigurationDocument, + desired: unknown, +): boolean => + clientConfigurationValuesEqual( + parsed.servers[PRODUCT_IDENTITY.mcpServerKey], + desired, + ) && !Object.hasOwn(parsed.legacyServers, PRODUCT_IDENTITY.mcpServerKey); + const clientConfigurationDesired = ( client: SetupClient, providerEnvironment: SetupProviderEnvironment, command: readonly string[], - format: NonNullable | undefined, + format: ClientRegistrationDialect | undefined, ) => { const environment = Object.fromEntries( Object.entries(providerEnvironment).sort(([left], [right]) => diff --git a/src/application/Uninstall.ts b/src/application/Uninstall.ts index 7bad3c6a3..648209488 100644 --- a/src/application/Uninstall.ts +++ b/src/application/Uninstall.ts @@ -1,7 +1,10 @@ import { parseClientConfiguration, serializeClientConfiguration, - type ClientServersKey, + clientServerPath, + legacyClientServerPath, + withClientServers, + type ClientConfigurationDocument, } from "./ClientConfigurationDocument.js"; import { copyFile, lstat, readFile, realpath, rm } from "node:fs/promises"; import { homedir } from "node:os"; @@ -141,15 +144,9 @@ const removeClient = async ( "Configuration could not be read. Check file permissions, then rerun uninstall.", ); } - let document: Record; - let key: ClientServersKey; - let servers: Record; + let parsed: ClientConfigurationDocument; try { - ({ - document, - servers, - serversKey: key, - } = parseClientConfiguration(original, client.format)); + parsed = parseClientConfiguration(original, client.format); } catch (cause: unknown) { void cause; return item( @@ -158,18 +155,34 @@ const removeClient = async ( `Configuration is not valid ${client.format === "toml" ? "TOML" : client.format === "opencode" ? "JSONC" : "JSON"} and was not changed. Repair it, then rerun uninstall.`, ); } - const registration = servers[PRODUCT_IDENTITY.mcpServerKey]; - if (registration === undefined) + const name = PRODUCT_IDENTITY.mcpServerKey; + // Beside a native OpenCode V2 table, OpenCode also loads a V1 `mcp.rea`. + const legacyPath = legacyClientServerPath(parsed, name); + const hasNative = Object.hasOwn(parsed.servers, name); + if (!hasNative && legacyPath === undefined) return item(client.name, "skipped", "REA registration is absent."); - if (!isOwnedRegistration(registration, client)) + const removeNative = + hasNative && isOwnedRegistration(parsed.servers[name], client); + const removeLegacy = + legacyPath !== undefined && + isOwnedRegistration(parsed.legacyServers[name], client); + if (!removeNative && !removeLegacy) return item( client.name, "retained", "The rea-named registration is not REA-owned.", ); - const remaining = { ...servers }; - delete remaining[PRODUCT_IDENTITY.mcpServerKey]; - document[key] = remaining; + const remaining = { ...parsed.servers }; + if (removeNative) delete remaining[name]; + const document = withClientServers( + parsed, + remaining, + removeLegacy ? name : undefined, + ); + const editedPaths = [ + ...(removeNative ? [clientServerPath(parsed, name)] : []), + ...(removeLegacy ? [legacyPath] : []), + ]; const backupPath = `${client.configPath}.rea.backup`; try { await fileSystem.copy(transactionPath, backupPath); @@ -188,14 +201,17 @@ const removeClient = async ( document, client.format, original, - PRODUCT_IDENTITY.mcpServerKey, + editedPaths, ), ); const readback = parseClientConfiguration( await fileSystem.readText(transactionPath), client.format, ); - if (PRODUCT_IDENTITY.mcpServerKey in readback.servers) + if ( + (removeNative && Object.hasOwn(readback.servers, name)) || + (removeLegacy && Object.hasOwn(readback.legacyServers, name)) + ) throw new Error("registration readback mismatch"); return item( client.name, diff --git a/src/application/UpdateMaintenance.ts b/src/application/UpdateMaintenance.ts index 78e1197b0..da44b353a 100644 --- a/src/application/UpdateMaintenance.ts +++ b/src/application/UpdateMaintenance.ts @@ -5,7 +5,10 @@ import { z } from "zod"; import { PRODUCT_IDENTITY } from "../identity.js"; import { isOwnedClientRegistrationCommand } from "./ClientRegistrationIdentity.js"; import { readClientRegistrationStatuses } from "./ClientRegistrationStatus.js"; -import { parseClientConfiguration } from "./ClientConfigurationDocument.js"; +import { + effectiveClientServer, + parseClientConfiguration, +} from "./ClientConfigurationDocument.js"; import { supportedClients } from "./SupportedClients.js"; import type { SetupAction } from "./SetupTypes.js"; import type { Result } from "../domain/result.js"; @@ -60,7 +63,7 @@ export const existingMaintenanceScope = async ( continue; const client = supported.find(({ name }) => name === registration.client); if (client === undefined) continue; - const { servers } = parseClientConfiguration( + const parsed = parseClientConfiguration( await readFile(client.configPath, "utf8"), client.format, ); @@ -69,7 +72,7 @@ export const existingMaintenanceScope = async ( enabled: z.boolean().optional(), disabled: z.boolean().optional(), }) - .parse(servers[PRODUCT_IDENTITY.mcpServerKey]); + .parse(effectiveClientServer(parsed, PRODUCT_IDENTITY.mcpServerKey)); if (enabled.enabled !== false && enabled.disabled !== true) clients.push(client.name); } diff --git a/tests/boundary/filesystem/clientConfigurationDialects.test.ts b/tests/boundary/filesystem/clientConfigurationDialects.test.ts index 53b5de096..106aba2f0 100644 --- a/tests/boundary/filesystem/clientConfigurationDialects.test.ts +++ b/tests/boundary/filesystem/clientConfigurationDialects.test.ts @@ -191,6 +191,157 @@ describe("additional client configuration dialects", () => { }); }); +const parseOpenCode = (text: string): Record => { + const errors: ParseError[] = []; + const document = parseJsonc(text, errors, { + allowTrailingComma: true, + }) as Record; + expect(errors).toEqual([]); + return document; +}; + +const openCodeClient = async (prefix: string) => { + const home = await createTestTempDirectory(prefix); + const client = getClients(home).find(({ name }) => name === "opencode"); + if (client === undefined) throw new Error("missing OpenCode client"); + await mkdir(client.markerPath ?? home, { recursive: true }); + return { home, client }; +}; + +const legacyEntry = JSON.stringify({ type: "local", command, enabled: true }); + +describe("OpenCode V2 native server table", () => { + it("registers in mcp.servers, replaces REA's V1 entry, and uninstalls", async () => { + const { home, client } = await openCodeClient("rea-opencode-v2-"); + await writeFile( + client.configPath, + `{ + "mcp": { + // Default timeouts are not a server. + "timeout": { "catalog": 30000 }, + "servers": { + // An independently managed server. + "other": { "type": "local", "command": ["node", "other.js"] }, + }, + "rea": ${legacyEntry}, + }, +}\n`, + ); + + expect( + (await configureClientConfiguration(client, {}, command)).status, + ).toBe("configured"); + expect(await configureClientConfiguration(client, {}, command)).toEqual({ + status: "unchanged", + }); + const text = await readFile(client.configPath, "utf8"); + expect(text).toContain("// Default timeouts are not a server."); + expect(text).toContain("// An independently managed server."); + expect(parseOpenCode(text).mcp).toEqual({ + timeout: { catalog: 30000 }, + servers: { + other: { type: "local", command: ["node", "other.js"] }, + rea: { type: "local", command: [...command] }, + }, + }); + expect(await readClientRegistrationStatuses(home)).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + client: "opencode", + state: "aligned", + command, + }), + ]), + ); + + expect((await systemUninstallHost(home).removeClient(client)).status).toBe( + "removed", + ); + expect( + parseOpenCode(await readFile(client.configPath, "utf8")).mcp, + ).toEqual({ + timeout: { catalog: 30000 }, + servers: { other: { type: "local", command: ["node", "other.js"] } }, + }); + }); + + it("updates and removes REA's entry after OpenCode migrates it to mcp.servers", async () => { + const { home, client } = await openCodeClient("rea-opencode-migrated-"); + const stale = ["npx", "-y", `${PRODUCT_IDENTITY.packageName}@3.2.0`, "mcp"]; + await writeFile( + client.configPath, + `${JSON.stringify({ mcp: { servers: { rea: { type: "local", command: stale } } } })}\n`, + ); + + expect(await readClientRegistrationStatuses(home)).toEqual( + expect.arrayContaining([ + expect.objectContaining({ client: "opencode", command: stale }), + ]), + ); + expect( + (await configureClientConfiguration(client, {}, command)).status, + ).toBe("configured"); + expect( + parseOpenCode(await readFile(client.configPath, "utf8")).mcp, + ).toEqual({ + servers: { rea: { type: "local", command: [...command] } }, + }); + expect((await systemUninstallHost(home).removeClient(client)).status).toBe( + "removed", + ); + expect( + parseOpenCode(await readFile(client.configPath, "utf8")).mcp, + ).toEqual({ + servers: {}, + }); + }); + + it("reports and removes a V1 entry that OpenCode still loads beside mcp.servers", async () => { + const { home, client } = await openCodeClient("rea-opencode-mixed-"); + await writeFile( + client.configPath, + `{ "mcp": { "servers": {}, "rea": ${legacyEntry} } }\n`, + ); + + expect(await readClientRegistrationStatuses(home)).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + client: "opencode", + state: "aligned", + command, + }), + ]), + ); + expect((await systemUninstallHost(home).removeClient(client)).status).toBe( + "removed", + ); + expect( + parseOpenCode(await readFile(client.configPath, "utf8")).mcp, + ).toEqual({ + servers: {}, + }); + }); + + it("keeps a V1 server named servers in the V1 table", async () => { + const { client } = await openCodeClient("rea-opencode-v1-servers-"); + const servers = { type: "local", command: ["node", "servers.js"] }; + await writeFile( + client.configPath, + `${JSON.stringify({ mcp: { servers } })}\n`, + ); + + expect( + (await configureClientConfiguration(client, {}, command)).status, + ).toBe("configured"); + expect( + parseOpenCode(await readFile(client.configPath, "utf8")).mcp, + ).toEqual({ + servers, + rea: { type: "local", command: [...command], enabled: true }, + }); + }); +}); + describe("platform-aware client config paths", () => { it.each([ [