diff --git a/src/components/mcp/McpFormModal.tsx b/src/components/mcp/McpFormModal.tsx index ec16275db..e5d3450f5 100644 --- a/src/components/mcp/McpFormModal.tsx +++ b/src/components/mcp/McpFormModal.tsx @@ -322,16 +322,21 @@ const McpFormModal: React.FC = ({ base?: McpServer, metaDraft?: MetaDraft, ): McpServer => { + // 只用「写入到」里能看到的勾选覆盖原值:隐藏只影响界面,覆盖已有服务器时隐藏 + // 应用的开关原样带回,否则会被当成关掉、从那些应用的配置里删掉;不支持 MCP 的 + // 应用(OpenClaw 等)沿用原值,新建时为 false + const visibleApps = Object.fromEntries( + visibleAppIds.map((app) => [app, apps[app]]), + ); const entry: McpServer = { ...(base ? { ...base } : {}), id, name: id, server: spec, - // 不支持 MCP 的应用(OpenClaw 等)沿用原值,新建时为 false apps: { openclaw: false, - ...(base?.apps ?? {}), - ...apps, + ...(base?.apps ?? appsOf(undefined, [])), + ...visibleApps, } as McpServer["apps"], }; if (!metaDraft) { diff --git a/tests/components/McpFormModal.test.tsx b/tests/components/McpFormModal.test.tsx index 48b25783d..14050d90b 100644 --- a/tests/components/McpFormModal.test.tsx +++ b/tests/components/McpFormModal.test.tsx @@ -185,6 +185,46 @@ describe("McpFormModal (drawer)", () => { expect(mocks.upsert.mock.calls[0][0]).toMatchObject({ id: "linear" }); }); + it("keeps hidden apps' flags when overwriting an existing server from a batch", async () => { + // Hermes 从侧栏隐藏了:覆盖 figma 时它在 Hermes 的开关要原样带回,不能被当成关掉 + renderForm({ + visibleAppIds: ["claude", "codex"], + existingServers: { + figma: { + id: "figma", + name: "figma", + server: { type: "http", url: "http://127.0.0.1:3845/mcp" }, + apps: { claude: false, hermes: true } as McpServer["apps"], + }, + }, + }); + fireEvent.change(screen.getByLabelText("mcpPage.drawer.paste"), { + target: { + value: JSON.stringify({ + mcpServers: { + linear: { type: "http", url: "https://mcp.linear.app/mcp" }, + figma: { type: "http", url: "http://127.0.0.1:3845/v2" }, + }, + }), + }, + }); + await userEvent.click( + screen.getByRole("button", { name: "mcpPage.drawer.batchOverwrite" }), + ); + await userEvent.click( + screen.getByRole("button", { name: "mcpPage.drawer.addManyReplace" }), + ); + await waitFor(() => expect(mocks.upsert).toHaveBeenCalledTimes(2)); + const figma = mocks.upsert.mock.calls + .map((call) => call[0]) + .find((entry) => entry.id === "figma"); + expect(figma.apps).toMatchObject({ + claude: true, + codex: true, + hermes: true, + }); + }); + it("masks secrets in the JSON tab and keeps them on save", async () => { const server: McpServer = { id: "serena",