You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/08/28 12:44:55 UTC

(maka) branch main updated: feat(tui): manage MCP servers from /mcp (#4062)

This is an automated email from the ASF dual-hosted git repository.

Astro-Han pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/main by this push:
     new 590d37cea feat(tui): manage MCP servers from /mcp (#4062)
590d37cea is described below

commit 590d37ceafef1f3cfeaba7ee1c3a906b38b5dc62
Author: Zhang <96...@users.noreply.github.com>
AuthorDate: Fri Aug 28 20:44:50 2026 +0800

    feat(tui): manage MCP servers from /mcp (#4062)
    
    * refactor(mcp): share credential retirement classification
    
    Generated-by: Codex
    
    * feat(cli): serialize TUI MCP management actions
    
    Generated-by: Codex
    
    * feat(tui): manage MCP servers from /mcp
    
    Generated-by: Codex
    
    * fix(mcp): serialize config mutations across processes
    
    Generated-by: Codex
    
    * fix(tui): keep MCP selection visible
    
    Generated-by: Codex
    
    * fix(mcp): recover config lock after process exit
    
    Generated-by: Codex
---
 .../src/main/__tests__/mcp-ipc-main.test.ts        |  73 +-
 apps/desktop/src/main/mcp-ipc-main.ts              |  78 +--
 .../cli/src/__tests__/pi-tui-mcp-status.test.ts    |  88 ++-
 packages/cli/src/__tests__/pi-tui-runner.test.ts   | 187 ++++++
 packages/cli/src/__tests__/tui-mcp-control.test.ts | 627 +++++++++++++++++-
 packages/cli/src/pi-tui-mcp-status.ts              | 731 +++++++++++++++++++--
 packages/cli/src/pi-tui-runner.ts                  |  12 +-
 packages/cli/src/runtime-host-tui-context.ts       |   4 +-
 packages/cli/src/tui-mcp-control.ts                | 411 +++++++++++-
 packages/cli/src/tui-primary-guidance.ts           |   4 +-
 .../__tests__/mcp-credential-retirement.test.ts    |  51 ++
 packages/core/src/mcp.ts                           |  14 +
 packages/mcp/src/index.ts                          |  20 +-
 .../__tests__/fixtures/mcp-config-lock-holder.ts   |  29 +
 .../storage/src/__tests__/mcp-config-store.test.ts |  63 +-
 packages/storage/src/mcp-config-store.ts           |  96 +--
 16 files changed, 2251 insertions(+), 237 deletions(-)

diff --git a/apps/desktop/src/main/__tests__/mcp-ipc-main.test.ts b/apps/desktop/src/main/__tests__/mcp-ipc-main.test.ts
index 107c66856..996cf0614 100644
--- a/apps/desktop/src/main/__tests__/mcp-ipc-main.test.ts
+++ b/apps/desktop/src/main/__tests__/mcp-ipc-main.test.ts
@@ -40,7 +40,7 @@ test('MCP IPC commits config before publishing capabilities and emitting status'
       get: async () => config,
       transform: async (apply) => {
         calls.push('store');
-        config = apply(config);
+        config = await apply(config);
         return config;
       },
       upsert: async (serverId, server) => {
@@ -111,12 +111,12 @@ test('MCP IPC commits config before publishing capabilities and emitting status'
   assert.equal(added.status, 'added');
   assert.deepEqual(added.config.mcpServers.brave, { command: 'npx' });
   assert.deepEqual(calls, ['store', 'sync', 'emit', 'publish']);
-  // A taken id comes back as data, not an IPC error, and commits nothing —
-  // the existence check now fails before the transaction ever reaches the
-  // credential-erase or write steps.
+  // A taken id comes back as data, not an IPC error. The check runs against
+  // the locked transaction snapshot, but reaches neither credential cleanup
+  // nor the file replacement.
   calls.length = 0;
   assert.deepEqual(await add({}, 'brave', { command: 'other' }), { status: 'exists' });
-  assert.deepEqual(calls, []);
+  assert.deepEqual(calls, ['store']);
 
   calls.length = 0;
   const testHandler = handlers.get('mcp:test');
@@ -130,7 +130,7 @@ test('MCP IPC commits config before publishing capabilities and emitting status'
   assert.ok(cancelInstall);
   const cancelled = await cancelInstall({}, 'fixture');
   assert.equal(cancelled.mcpServers.fixture, undefined);
-  assert.deepEqual(calls, ['cancel', 'forget', 'store', 'sync', 'emit', 'publish']);
+  assert.deepEqual(calls, ['cancel', 'store', 'forget', 'sync', 'emit', 'publish']);
 });
 
 test('MCP remove aborts before touching the config when credential deletion fails', async () => {
@@ -142,7 +142,7 @@ test('MCP remove aborts before touching the config when credential deletion fail
     store: {
       get: async () => config,
       transform: async (apply) => {
-        config = apply(config);
+        config = await apply(config);
         return config;
       },
       upsert: async (serverId, server) => {
@@ -209,7 +209,7 @@ test('MCP IPC redacts clientSecret toward the renderer and restores the sentinel
     store: {
       get: async () => config,
       transform: async (apply) => {
-        config = apply(config);
+        config = await apply(config);
         return config;
       },
       upsert: async (serverId, server) => {
@@ -315,11 +315,13 @@ test('MCP market cancellation waits for an in-flight config write before rolling
     store: {
       get: async () => config,
       transform: async (apply) => {
-        calls.push('write:start');
+        calls.push('transaction:start');
         markWriteStarted();
         await writeGate;
-        config = apply(config);
-        calls.push('write:end');
+        const next = await apply(config);
+        calls.push('write');
+        config = next;
+        calls.push('transaction:end');
         return config;
       },
       upsert: async (serverId, server) => {
@@ -372,8 +374,8 @@ test('MCP market cancellation waits for an in-flight config write before rolling
   // The cancellation's own removal is a full transaction on the same lane:
   // credentials retire first, then the conditional write.
   assert.deepEqual(calls, [
-    'write:start', 'cancel', 'write:end',
-    'forget', 'write:start', 'write:end',
+    'transaction:start', 'cancel', 'write', 'transaction:end',
+    'transaction:start', 'forget', 'write', 'transaction:end',
     'sync', 'emit', 'publish',
   ]);
 });
@@ -394,7 +396,7 @@ test('an active login on a secret-bearing server does not veto edits to another
     ipcMain: { handle(channel, handler) { handlers.set(channel, handler as (...args: any[]) => Promise<any>); } },
     store: {
       get: async () => config,
-      transform: async (apply) => { config = apply(config); return config; },
+      transform: async (apply) => { config = await apply(config); return config; },
       upsert: async (_serverId, _server) => config,
       remove: async () => config,
     },
@@ -452,7 +454,13 @@ test('a URL change retires the old endpoint credentials before the write, and an
     ipcMain: { handle(channel, handler) { handlers.set(channel, handler as (...args: any[]) => Promise<any>); } },
     store: {
       get: async () => config,
-      transform: async (apply) => { calls.push('write'); config = apply(config); return config; },
+      transform: async (apply) => {
+        calls.push('transaction:start');
+        const next = await apply(config);
+        calls.push('write');
+        config = next;
+        return config;
+      },
       upsert: async (_serverId, _server) => config,
       remove: async () => config,
     },
@@ -487,20 +495,29 @@ test('a URL change retires the old endpoint credentials before the write, and an
     upsert({}, 'remote', { url: 'https://new.example.com/mcp' }),
     /credential store unavailable/u,
   );
-  assert.deepEqual(calls, ['forget']);
+  assert.deepEqual(calls, ['transaction:start', 'forget']);
   const kept = config.mcpServers.remote;
   assert.ok(kept && 'url' in kept);
   assert.equal(kept.url, 'https://old.example.com/mcp');
 
+  // Policy failures are known before the credential-first transaction: an
+  // invalid replacement must not log the user out when it cannot be saved.
+  calls.length = 0;
+  await assert.rejects(
+    upsert({}, 'remote', { url: 'http://public.example.com/mcp' }),
+    /must use https/u,
+  );
+  assert.deepEqual(calls, ['transaction:start']);
+
   // Same repoint with a healthy credential store: erase strictly precedes
   // the write. An unchanged-URL upsert afterwards does not erase at all.
   eraseFails = false;
   calls.length = 0;
   await upsert({}, 'remote', { url: 'https://new.example.com/mcp' });
-  assert.deepEqual(calls, ['forget', 'write', 'sync']);
+  assert.deepEqual(calls, ['transaction:start', 'forget', 'write', 'sync']);
   calls.length = 0;
   await upsert({}, 'remote', { url: 'https://new.example.com/mcp', enabled: false });
-  assert.deepEqual(calls, ['write', 'sync']);
+  assert.deepEqual(calls, ['transaction:start', 'write', 'sync']);
 });
 
 test('cancelling an install rolls back only its own write, never a newer same-id config', async () => {
@@ -513,7 +530,7 @@ test('cancelling an install rolls back only its own write, never a newer same-id
     ipcMain: { handle(channel, handler) { handlers.set(channel, handler as (...args: any[]) => Promise<any>); } },
     store: {
       get: async () => config,
-      transform: async (apply) => { config = apply(config); return config; },
+      transform: async (apply) => { config = await apply(config); return config; },
       upsert: async (_serverId, _server) => config,
       remove: async () => config,
     },
@@ -586,7 +603,7 @@ test('a login claim travels the shared lane and cannot land inside an open trans
       transform: async (apply) => {
         markWriteStarted();
         await writeGate;
-        config = apply(config);
+        config = await apply(config);
         return config;
       },
       upsert: async (_serverId, _server) => config,
@@ -698,7 +715,7 @@ test('cancelling an install through the REAL store rolls the entry back despite
   }
 });
 
-test('the config write fails closed when the snapshot drifts under the transaction', async () => {
+test('the config commit applies its mutation to the transaction snapshot', async () => {
   const handlers = new Map<string, (...args: any[]) => Promise<any>>();
   const config: McpConfigFile = { version: MCP_CONFIG_VERSION, mcpServers: {} };
   const drifted: McpConfigFile = {
@@ -710,10 +727,10 @@ test('the config write fails closed when the snapshot drifts under the transacti
     ipcMain: { handle(channel, handler) { handlers.set(channel, handler as (...args: any[]) => Promise<any>); } },
     store: {
       get: async () => config,
-      // Simulates an out-of-band writer landing between the snapshot read
-      // and the serialized write: apply() observes a different config.
+      // The store supplies the current snapshot after acquiring its shared
+      // lock. The mutation must preserve an unrelated edit already in it.
       transform: async (apply) => {
-        const next = apply(drifted);
+        const next = await apply(drifted);
         wrote = true;
         return next;
       },
@@ -742,8 +759,10 @@ test('the config write fails closed when the snapshot drifts under the transacti
 
   const upsert = handlers.get('mcp:upsert');
   assert.ok(upsert);
-  await assert.rejects(upsert({}, 'fixture', { command: 'node' }), /changed while/u);
-  assert.equal(wrote, false);
+  const next = await upsert({}, 'fixture', { command: 'node' });
+  assert.equal(wrote, true);
+  assert.ok(next.mcpServers.intruder);
+  assert.ok(next.mcpServers.fixture);
 });
 
 test('MCP config commit is not rolled back by a capability publication failure', async () => {
@@ -759,7 +778,7 @@ test('MCP config commit is not rolled back by a capability publication failure',
     store: {
       get: async () => config,
       transform: async (apply) => {
-        config = apply(config);
+        config = await apply(config);
         return config;
       },
       upsert: async (serverId, server) => {
diff --git a/apps/desktop/src/main/mcp-ipc-main.ts b/apps/desktop/src/main/mcp-ipc-main.ts
index 7b0a1dbed..0cb59d44f 100644
--- a/apps/desktop/src/main/mcp-ipc-main.ts
+++ b/apps/desktop/src/main/mcp-ipc-main.ts
@@ -20,7 +20,7 @@
 import type { IpcMain } from 'electron';
 import {
   MCP_CONFIG_VERSION,
-  isMcpStdioConfig,
+  mcpConfigChangeRetiresCredentials,
   type McpConfigAddResult,
   type McpConfigFile,
   type McpConfigImportResult,
@@ -29,6 +29,7 @@ import {
 } from '@maka/core/mcp';
 import type { McpClientManager } from '@maka/mcp';
 import {
+  assertMcpEndpointPolicyOnChanges,
   McpServerExistsError,
   McpConfigSourceError,
   normalizeMcpConfig,
@@ -93,53 +94,45 @@ export function registerMcpIpcMain(deps: McpIpcMainDeps): void {
       );
     }
   };
-  // Every config mutation is one transaction on one lane:
+  // Every config mutation is one transaction on one lane and one shared file
+  // lock:
   //   read the authoritative snapshot → restore sentinels and apply the
   //   active-login gate against it → erase the credentials this commit
   //   orphans (removed servers, repointed endpoints) → persist.
-  // The credential erasure is asynchronous, so it cannot live inside the
-  // store's synchronous transform; the lane serializes the whole sequence
-  // instead, and the final transform still fails closed if the snapshot
-  // drifted under an out-of-band writer. Credentials go first so a failed
-  // erase aborts the commit while everything is still configured and
-  // retryable — never a persisted removal whose token a same-id re-add
-  // could inherit after a restart.
+  // The lane also excludes Desktop OAuth claims; the store transaction makes
+  // the same sequence linearizable against TUI and other process writers.
+  // Credentials go first so a failed erase aborts the commit while everything
+  // is still configured and retryable — never a persisted removal whose token
+  // a same-id re-add could inherit after a restart.
   const inMutationLane = deps.exclusiveLane ?? createMcpExclusiveLane();
   const commitConfig = async (
     mutate: (current: McpConfigFile) => McpConfigFile,
-  ): Promise<McpConfigFile> => {
-    const current = await deps.store.get();
-    const next = mutate(current);
-    // The authoritative gate: every server this commit semantically touches
-    // is re-checked INSIDE the lane. The handler-entry checks are advisory
-    // fast-fails; this one cannot race a login claim, because claims travel
-    // the same lane.
-    for (const serverId of new Set([
-      ...Object.keys(current.mcpServers),
-      ...Object.keys(next.mcpServers),
-    ])) {
-      const before = current.mcpServers[serverId];
-      const after = next.mcpServers[serverId];
-      if (JSON.stringify(before) !== JSON.stringify(after)) assertNoActiveLogin(serverId);
-    }
-    // Erases are per-server and not transactional as a set: if one fails
-    // partway, the commit aborts with the EARLIER servers already logged
-    // out. That partial effect is deliberately in the fail-closed direction
-    // — a re-login is recoverable, a credential outliving its removed or
-    // repointed config is not.
-    for (const serverId of credentialRetirements(current, next)) {
-      await deps.manager.forgetServerCredentials(serverId);
-    }
-    const snapshot = JSON.stringify(current);
-    return deps.store.transform((actual) => {
-      if (JSON.stringify(actual) !== snapshot) {
-        throw new Error(
-          'MCP configuration changed while this update was being prepared — retry the operation',
-        );
+  ): Promise<McpConfigFile> =>
+    deps.store.transform(async (current) => {
+      const next = mutate(current);
+      assertMcpEndpointPolicyOnChanges(current, next);
+      // The authoritative gate: every server this commit semantically touches
+      // is re-checked INSIDE the lane. The handler-entry checks are advisory
+      // fast-fails; this one cannot race a login claim, because claims travel
+      // the same lane.
+      for (const serverId of new Set([
+        ...Object.keys(current.mcpServers),
+        ...Object.keys(next.mcpServers),
+      ])) {
+        const before = current.mcpServers[serverId];
+        const after = next.mcpServers[serverId];
+        if (JSON.stringify(before) !== JSON.stringify(after)) assertNoActiveLogin(serverId);
+      }
+      // Erases are per-server and not transactional as a set: if one fails
+      // partway, the commit aborts with the EARLIER servers already logged
+      // out. That partial effect is deliberately in the fail-closed direction
+      // — a re-login is recoverable, a credential outliving its removed or
+      // repointed config is not.
+      for (const serverId of credentialRetirements(current, next)) {
+        await deps.manager.forgetServerCredentials(serverId);
       }
       return next;
     });
-  };
   // The renderer is semi-trusted (SECURITY.md §3): every config that crosses
   // toward it leaves with clientSecret replaced by the sentinel, and every
   // config it sends back has sentinels restored from disk before the store
@@ -369,12 +362,7 @@ function credentialRetirements(current: McpConfigFile, next: McpConfigFile): str
     const incoming = Object.hasOwn(next.mcpServers, serverId)
       ? next.mcpServers[serverId]
       : undefined;
-    if (!incoming) {
-      retired.push(serverId);
-      continue;
-    }
-    if (isMcpStdioConfig(server)) continue;
-    if (isMcpStdioConfig(incoming) || incoming.url !== server.url) retired.push(serverId);
+    if (mcpConfigChangeRetiresCredentials(server, incoming)) retired.push(serverId);
   }
   return retired;
 }
diff --git a/packages/cli/src/__tests__/pi-tui-mcp-status.test.ts b/packages/cli/src/__tests__/pi-tui-mcp-status.test.ts
index e7f43d7c1..81770f71b 100644
--- a/packages/cli/src/__tests__/pi-tui-mcp-status.test.ts
+++ b/packages/cli/src/__tests__/pi-tui-mcp-status.test.ts
@@ -19,21 +19,24 @@
 
 import assert from 'node:assert/strict';
 import { describe, test } from 'node:test';
-import { McpStatusOverlay } from '../pi-tui-mcp-status.js';
-import type { TuiMcpSnapshot, TuiMcpSurface } from '../tui-mcp-control.js';
+import { McpManagementOverlay } from '../pi-tui-mcp-status.js';
+import type { TuiMcpAction, TuiMcpManagement, TuiMcpSnapshot } from '../tui-mcp-control.js';
 import { stripAnsi } from '../tui-ansi.js';
 
-describe('MCP status overlay', () => {
+describe('MCP management overlay', () => {
   test('renders the local publication and negotiated server status', () => {
-    const overlay = new McpStatusOverlay({
+    const overlay = new McpManagementOverlay({
       locale: 'en',
       surface: surface({
         initialization: 'ready',
+        configuration: 'ready',
         publication: 'published',
         toolCount: 2,
         servers: [
           {
             serverId: 'filesystem',
+            configured: true,
+            synchronized: true,
             state: 'connected',
             transport: 'stdio',
             negotiatedProtocol: { era: 'modern', revision: '2026-07-28' },
@@ -52,7 +55,7 @@ describe('MCP status overlay', () => {
   });
 
   test('states the remote limitation instead of implying an empty local config', () => {
-    const overlay = new McpStatusOverlay({
+    const overlay = new McpManagementOverlay({
       locale: 'zh',
       viewportRows: () => 6,
       onClose: () => undefined,
@@ -66,14 +69,22 @@ describe('MCP status overlay', () => {
   });
 
   test('localizes manager states without changing their source values', () => {
-    const overlay = new McpStatusOverlay({
+    const overlay = new McpManagementOverlay({
       locale: 'zh',
       surface: surface({
         initialization: 'ready',
+        configuration: 'ready',
         publication: 'not_published',
         toolCount: 0,
         servers: [
-          { serverId: 'oauth', state: 'needs-auth', transport: 'streamable-http', toolCount: 0 },
+          {
+            serverId: 'oauth',
+            configured: true,
+            synchronized: true,
+            state: 'needs-auth',
+            transport: 'streamable-http',
+            toolCount: 0,
+          },
         ],
       }),
       viewportRows: () => 6,
@@ -92,6 +103,7 @@ describe('MCP status overlay', () => {
     let closed = 0;
     const mcp = surface({
       initialization: 'ready',
+      configuration: 'ready',
       publication: 'not_published',
       toolCount: 0,
       servers: [],
@@ -102,7 +114,7 @@ describe('MCP status overlay', () => {
         disposed += 1;
       };
     };
-    const overlay = new McpStatusOverlay({
+    const overlay = new McpManagementOverlay({
       locale: 'en',
       surface: mcp,
       viewportRows: () => 6,
@@ -117,13 +129,71 @@ describe('MCP status overlay', () => {
     assert.equal(disposed, 1);
     assert.equal(closed, 1);
   });
+
+  test('keeps long-list selection visible and applies actions to the visible server', async () => {
+    const actions: TuiMcpAction[] = [];
+    const mcp = surface({
+      initialization: 'ready',
+      configuration: 'ready',
+      publication: 'not_published',
+      toolCount: 0,
+      servers: Array.from({ length: 8 }, (_, index) => ({
+        serverId: `s${index}`,
+        configured: true,
+        synchronized: true,
+        enabled: true,
+        configuredTransport: 'stdio' as const,
+        configuredProtocol: 'legacy' as const,
+        ...(index === 5 ? { state: 'error' as const, error: 'visible diagnostic' } : {}),
+        toolCount: 0,
+      })),
+    });
+    mcp.execute = async (action) => {
+      actions.push(action);
+      return { status: 'applied', effect: 'published' };
+    };
+    const overlay = new McpManagementOverlay({
+      locale: 'en',
+      surface: mcp,
+      viewportRows: () => 6,
+      onClose: () => undefined,
+      onChange: () => undefined,
+    });
+    overlay.render(100);
+
+    for (let index = 0; index < 5; index += 1) overlay.handleInput('\u001b[B');
+    let text = overlay.render(100).map(stripAnsi).join('\n');
+    assert.match(text, /› ● s5/u);
+    assert.match(text, /visible diagnostic/u);
+
+    overlay.handleInput(' ');
+    await new Promise<void>((resolve) => setImmediate(resolve));
+    assert.deepEqual(actions, [{ kind: 'set_enabled', serverId: 's5', enabled: false }]);
+
+    overlay.handleInput('\u001b[H');
+    text = overlay.render(100).map(stripAnsi).join('\n');
+    assert.match(text, /› ○ s0/u);
+    overlay.handleInput('\u001b[6~');
+    text = overlay.render(100).map(stripAnsi).join('\n');
+    assert.match(text, /› ○ s4/u);
+    overlay.handleInput('\u001b[F');
+    text = overlay.render(100).map(stripAnsi).join('\n');
+    assert.match(text, /› ○ s7/u);
+    overlay.handleInput('\u001b[5~');
+    text = overlay.render(100).map(stripAnsi).join('\n');
+    assert.match(text, /› ○ s4/u);
+  });
 });
 
 function surface(
   snapshot: TuiMcpSnapshot,
-): TuiMcpSurface & { subscribe(listener: () => void): () => void } {
+): TuiMcpManagement & { subscribe(listener: () => void): () => void } {
   return {
     snapshot: () => snapshot,
     subscribe: () => () => undefined,
+    configForEdit: () => undefined,
+    previewImport: () => ({ status: 'invalid', reason: 'invalid-config' }),
+    discardImportPreview: () => undefined,
+    execute: async () => ({ status: 'failed', reason: 'manager-failed' }),
   };
 }
diff --git a/packages/cli/src/__tests__/pi-tui-runner.test.ts b/packages/cli/src/__tests__/pi-tui-runner.test.ts
index f91bfe575..1c0b3eb28 100644
--- a/packages/cli/src/__tests__/pi-tui-runner.test.ts
+++ b/packages/cli/src/__tests__/pi-tui-runner.test.ts
@@ -77,6 +77,7 @@ import {
 import { AUTO_RECAP_IDLE_MS } from '../session-recap.js';
 import { BUSY_SPINNER_FRAMES } from '../tui-attention.js';
 import { EXPANSION_COLLAPSE_CONFIRM_WINDOW_MS } from '../pi-transcript.js';
+import type { TuiMcpAction, TuiMcpManagement } from '../tui-mcp-control.js';
 import {
   autocompleteSuggestionLines,
   assertBottomPickerPlacement,
@@ -2219,11 +2220,14 @@ describe('Maka Pi TUI runner', () => {
       mcp: {
         snapshot: () => ({
           initialization: 'ready',
+          configuration: 'ready',
           publication: 'published',
           toolCount: 1,
           servers: [
             {
               serverId: 'filesystem',
+              configured: true,
+              synchronized: true,
               state: 'connected',
               transport: 'stdio',
               negotiatedProtocol: { era: 'modern', revision: '2026-07-28' },
@@ -2232,6 +2236,10 @@ describe('Maka Pi TUI runner', () => {
           ],
         }),
         subscribe: () => () => undefined,
+        configForEdit: () => undefined,
+        previewImport: () => ({ status: 'invalid', reason: 'invalid-config' }),
+        discardImportPreview: () => undefined,
+        execute: async () => ({ status: 'failed', reason: 'manager-failed' }),
       },
     });
 
@@ -2245,6 +2253,185 @@ describe('Maka Pi TUI runner', () => {
     await run;
   });
 
+  test('/mcp previews pasted JSON and completes guided setup in-frame', async () => {
+    const terminal = new FakeTerminal();
+    const driver = new SlashCommandDriver();
+    const actions: TuiMcpAction[] = [];
+    const discardedPreviews: string[] = [];
+    let previewSource = '';
+    const mcp: TuiMcpManagement = {
+      snapshot: () => ({
+        initialization: 'ready',
+        configuration: 'ready',
+        publication: 'not_published',
+        toolCount: 0,
+        servers: [],
+      }),
+      subscribe: () => () => undefined,
+      configForEdit: () => undefined,
+      previewImport: (source) => {
+        previewSource = source;
+        return {
+          status: 'ready',
+          preview: {
+            previewId: 'preview-1',
+            entries: [
+              {
+                serverId: 'docs',
+                change: 'add',
+                transport: 'remote',
+                protocol: 'auto',
+              },
+            ],
+          },
+        };
+      },
+      discardImportPreview: (previewId) => discardedPreviews.push(previewId),
+      execute: async (action) => {
+        actions.push(action);
+        return { status: 'applied', effect: 'published' };
+      },
+    };
+    const run = runMakaPiTui({
+      title: 'Maka',
+      driver,
+      cwd: '/repo',
+      model: 'deepseek-v4-flash',
+      connectionSlug: 'deepseek',
+      permissionMode: 'ask',
+      terminal,
+      mcp,
+    });
+
+    terminal.input('/mcp');
+    terminal.input('\r');
+    await waitFor(() => plainTerminalOutput(terminal.screenOutput()).includes('MCP SERVERS'));
+    terminal.input('a');
+    terminal.input('j');
+    terminal.input('secret-draft');
+    await waitFor(() => plainTerminalOutput(terminal.screenOutput()).includes('secret-draft'));
+    terminal.input('\x1b');
+    await waitFor(() => !plainTerminalOutput(terminal.screenOutput()).includes('secret-draft'));
+    terminal.input('a');
+    terminal.input('j');
+    terminal.input('\x1f');
+    await delay(0);
+    assert.doesNotMatch(plainTerminalOutput(terminal.screenOutput()), /secret-draft/u);
+    terminal.input('{"docs":{"url":"https://docs.example/mcp","protocol":"auto"}}');
+    terminal.input('\r');
+    await waitFor(() =>
+      plainTerminalOutput(terminal.screenOutput()).includes('Import MCP servers?'),
+    );
+    assert.equal(actions.length, 0);
+    assert.equal(previewSource.includes('docs.example'), true);
+    terminal.input('\x1b');
+    assert.deepEqual(discardedPreviews, ['preview-1']);
+    terminal.input('a');
+    terminal.input('j');
+    terminal.input('{"docs":{"url":"https://docs.example/mcp","protocol":"auto"}}');
+    terminal.input('\r');
+    await waitFor(() =>
+      plainTerminalOutput(terminal.screenOutput()).includes('Import MCP servers?'),
+    );
+    terminal.input('y');
+    await waitFor(() => actions.length === 1);
+    assert.deepEqual(actions[0], { kind: 'commit_import', previewId: 'preview-1' });
+    await waitFor(() => plainTerminalOutput(terminal.screenOutput()).includes('tools refreshed'));
+
+    terminal.input('a');
+    terminal.input('g');
+    terminal.input('local');
+    terminal.input('\r');
+    terminal.input('1');
+    terminal.input('mcp-server');
+    terminal.input('\r');
+    terminal.input('[]');
+    terminal.input('\r');
+    terminal.input('3');
+    terminal.input('\r');
+    terminal.input('{}');
+    terminal.input('\r');
+    await waitFor(() =>
+      plainTerminalOutput(terminal.screenOutput()).includes('Add this MCP server?'),
+    );
+    terminal.input('y');
+    await waitFor(() => actions.length === 2);
+    assert.deepEqual(actions[1], {
+      kind: 'add',
+      serverId: 'local',
+      config: {
+        enabled: true,
+        command: 'mcp-server',
+        args: [],
+        env: {},
+        protocol: '2026-07-28',
+      },
+    });
+    terminal.input('q');
+    exitMaka(terminal);
+    await run;
+  });
+
+  test('/mcp ignores a late action result after the user cancels its busy view', async () => {
+    const terminal = new FakeTerminal();
+    const driver = new SlashCommandDriver();
+    const result = deferred<Awaited<ReturnType<TuiMcpManagement['execute']>>>();
+    const actions: TuiMcpAction[] = [];
+    const mcp: TuiMcpManagement = {
+      snapshot: () => ({
+        initialization: 'ready',
+        configuration: 'ready',
+        publication: 'not_published',
+        toolCount: 0,
+        servers: [
+          {
+            serverId: 'docs',
+            configured: true,
+            synchronized: true,
+            enabled: false,
+            configuredTransport: 'remote',
+            configuredProtocol: 'auto',
+            state: 'disabled',
+            toolCount: 0,
+          },
+        ],
+      }),
+      subscribe: () => () => undefined,
+      configForEdit: () => undefined,
+      previewImport: () => ({ status: 'invalid', reason: 'invalid-config' }),
+      discardImportPreview: () => undefined,
+      execute: async (action) => {
+        actions.push(action);
+        return result.promise;
+      },
+    };
+    const run = runMakaPiTui({
+      title: 'Maka',
+      driver,
+      cwd: '/repo',
+      model: 'deepseek-v4-flash',
+      connectionSlug: 'deepseek',
+      permissionMode: 'ask',
+      terminal,
+      mcp,
+    });
+
+    terminal.input('/mcp');
+    terminal.input('\r');
+    await waitFor(() => plainTerminalOutput(terminal.screenOutput()).includes('docs'));
+    terminal.input(' ');
+    await waitFor(() => actions.length === 1);
+    assert.deepEqual(actions[0], { kind: 'set_enabled', serverId: 'docs', enabled: true });
+    terminal.input('\x1b');
+    await waitFor(() => plainTerminalOutput(terminal.screenOutput()).includes('docs'));
+    result.resolve({ status: 'failed', reason: 'manager-failed' });
+    await delay(0);
+    assert.doesNotMatch(plainTerminalOutput(terminal.screenOutput()), /connection action failed/u);
+    terminal.input('q');
+    exitMaka(terminal);
+    await run;
+  });
+
   test('clears an unsent draft on Ctrl-C without closing Maka', async () => {
     const terminal = new FakeTerminal();
     const driver = new SlashCommandDriver();
diff --git a/packages/cli/src/__tests__/tui-mcp-control.test.ts b/packages/cli/src/__tests__/tui-mcp-control.test.ts
index ed7b04f55..d8836dcf9 100644
--- a/packages/cli/src/__tests__/tui-mcp-control.test.ts
+++ b/packages/cli/src/__tests__/tui-mcp-control.test.ts
@@ -18,6 +18,9 @@
  */
 
 import assert from 'node:assert/strict';
+import { mkdtemp, rm } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
 import test from 'node:test';
 import type { McpConfigFile, McpServerStatus, McpToolSnapshot } from '@maka/core/mcp';
 import type { McpClientManager } from '@maka/mcp';
@@ -25,6 +28,7 @@ import type {
   ClientCapabilityProvider,
   RuntimeHostConnectionAvailability,
 } from '@maka/runtime-host/client';
+import { createMcpConfigStore } from '@maka/storage/mcp-config-store';
 import { createTuiMcpController } from '../tui-mcp-control.js';
 
 test('TUI MCP startup stays backgrounded and publishes the discovered snapshot', async () => {
@@ -34,7 +38,7 @@ test('TUI MCP startup stays backgrounded and publishes the discovered snapshot',
   const controller = createTuiMcpController(
     { workspaceRoot: '/unused', connection: connection.connection },
     {
-      configStore: { get: () => config.promise },
+      configStore: configStoreHarness(() => config.promise),
       manager: manager.manager,
       createProvider: () => provider('provider-1'),
     },
@@ -49,6 +53,8 @@ test('TUI MCP startup stays backgrounded and publishes the discovered snapshot',
   assert.deepEqual(controller.snapshot().servers, [
     {
       serverId: 'local',
+      configured: false,
+      synchronized: false,
       state: 'connected',
       transport: 'stdio',
       negotiatedProtocol: { era: 'legacy', revision: '2024-11-05' },
@@ -73,7 +79,7 @@ test('TUI MCP publication coalesces a discovery change behind the in-flight revi
   const controller = createTuiMcpController(
     { workspaceRoot: '/unused', connection: connection.connection },
     {
-      configStore: { get: async () => emptyConfig() },
+      configStore: configStoreHarness(async () => emptyConfig()),
       manager: manager.manager,
       createProvider: () => {
         const id = `provider-${providers.length + 1}`;
@@ -98,7 +104,7 @@ test('TUI MCP invalidates a lost generation and republishes on its replacement',
   const controller = createTuiMcpController(
     { workspaceRoot: '/unused', connection: connection.connection },
     {
-      configStore: { get: async () => emptyConfig() },
+      configStore: configStoreHarness(async () => emptyConfig()),
       manager: manager.manager,
       createProvider: () => provider(`provider-${connection.replacements.length + 1}`),
     },
@@ -119,7 +125,7 @@ test('TUI MCP unregisters the current generation when discovery removes every to
   const controller = createTuiMcpController(
     { workspaceRoot: '/unused', connection: connection.connection },
     {
-      configStore: { get: async () => emptyConfig() },
+      configStore: configStoreHarness(async () => emptyConfig()),
       manager: manager.manager,
       createProvider: (current) =>
         current.toolSnapshot().tools.length === 0 ? undefined : provider('provider'),
@@ -140,11 +146,9 @@ test('TUI MCP fails closed when its saved config cannot be read', async () => {
   const controller = createTuiMcpController(
     { workspaceRoot: '/unused', connection: connection.connection },
     {
-      configStore: {
-        get: async () => {
-          throw new Error('config contains secret-value');
-        },
-      },
+      configStore: configStoreHarness(async () => {
+        throw new Error('config contains secret-value');
+      }),
       manager: manager.manager,
       createProvider: () => provider('must-not-publish'),
     },
@@ -168,7 +172,7 @@ test('TUI MCP unregisters a publication that settles while close is waiting', as
   const controller = createTuiMcpController(
     { workspaceRoot: '/unused', connection: connection.connection },
     {
-      configStore: { get: async () => emptyConfig() },
+      configStore: configStoreHarness(async () => emptyConfig()),
       manager: manager.manager,
       createProvider: () => provider('provider'),
     },
@@ -182,6 +186,588 @@ test('TUI MCP unregisters a publication that settles while close is waiting', as
   assert.equal(manager.closed, 1);
 });
 
+test('TUI MCP add commits before manager synchronization and reports Host convergence', async () => {
+  const order: string[] = [];
+  const store = mutableConfigStore(emptyConfig(), order);
+  const manager = managementManager(order);
+  const connection = connectionHarness();
+  const controller = createTuiMcpController(
+    { workspaceRoot: '/unused', connection: connection.connection },
+    {
+      configStore: store.store,
+      manager: manager.manager,
+      createProvider: () => undefined,
+    },
+  );
+  await waitFor(() => controller.snapshot().initialization === 'ready');
+  order.length = 0;
+
+  const result = await controller.execute({
+    kind: 'add',
+    serverId: 'docs',
+    config: { url: 'https://docs.example/mcp', protocol: 'auto' },
+  });
+
+  assert.deepEqual(result, { status: 'applied', effect: 'published' });
+  assert.deepEqual(order, ['transform', 'sync']);
+  assert.deepEqual((await store.store.get()).mcpServers.docs, {
+    enabled: true,
+    url: 'https://docs.example/mcp',
+    transport: 'auto',
+    protocol: 'auto',
+  });
+  assert.equal(controller.snapshot().configuration, 'ready');
+  await controller.close();
+});
+
+test('TUI MCP retires endpoint credentials before persistence and aborts on cleanup failure', async () => {
+  const order: string[] = [];
+  const store = mutableConfigStore(
+    {
+      version: 3,
+      mcpServers: { docs: { url: 'https://old.example/mcp' } },
+    },
+    order,
+  );
+  const manager = managementManager(order, { credentialFailure: true });
+  const connection = connectionHarness();
+  const controller = createTuiMcpController(
+    { workspaceRoot: '/unused', connection: connection.connection },
+    { configStore: store.store, manager: manager.manager, createProvider: () => undefined },
+  );
+  await waitFor(() => controller.snapshot().initialization === 'ready');
+  order.length = 0;
+  const edit = controller.configForEdit('docs');
+  assert.ok(edit);
+
+  const result = await controller.execute({
+    kind: 'edit',
+    serverId: 'docs',
+    expectedRevision: edit.revision,
+    config: { url: 'https://new.example/mcp' },
+  });
+
+  assert.deepEqual(result, { status: 'failed', reason: 'credential-cleanup-failed' });
+  assert.deepEqual(order, ['transform', 'forget:docs']);
+  const stored = (await store.store.get()).mcpServers.docs;
+  assert.ok(stored && 'url' in stored);
+  assert.equal(stored.url, 'https://old.example/mcp');
+  await controller.close();
+});
+
+test('TUI MCP rejects an invalid endpoint before retiring the previous credentials', async () => {
+  const order: string[] = [];
+  const store = mutableConfigStore(
+    { version: 3, mcpServers: { docs: { url: 'https://old.example/mcp' } } },
+    order,
+  );
+  const manager = managementManager(order);
+  const connection = connectionHarness();
+  const controller = createTuiMcpController(
+    { workspaceRoot: '/unused', connection: connection.connection },
+    { configStore: store.store, manager: manager.manager, createProvider: () => undefined },
+  );
+  await waitFor(() => controller.snapshot().initialization === 'ready');
+  const edit = controller.configForEdit('docs');
+  assert.ok(edit);
+  order.length = 0;
+
+  assert.deepEqual(
+    await controller.execute({
+      kind: 'edit',
+      serverId: 'docs',
+      expectedRevision: edit.revision,
+      config: { url: 'http://public.example/mcp' },
+    }),
+    { status: 'failed', reason: 'invalid-config' },
+  );
+  assert.deepEqual(order, ['transform']);
+  await controller.close();
+});
+
+test('TUI MCP edit rejects a stale revision without touching credentials or disk', async () => {
+  const order: string[] = [];
+  const store = mutableConfigStore(
+    { version: 3, mcpServers: { docs: { url: 'https://one.example/mcp' } } },
+    order,
+  );
+  const manager = managementManager(order);
+  const connection = connectionHarness();
+  const controller = createTuiMcpController(
+    { workspaceRoot: '/unused', connection: connection.connection },
+    { configStore: store.store, manager: manager.manager, createProvider: () => undefined },
+  );
+  await waitFor(() => controller.snapshot().initialization === 'ready');
+  const edit = controller.configForEdit('docs');
+  assert.ok(edit);
+  store.replace({ version: 3, mcpServers: { docs: { url: 'https://other.example/mcp' } } });
+  order.length = 0;
+
+  const result = await controller.execute({
+    kind: 'edit',
+    serverId: 'docs',
+    expectedRevision: edit.revision,
+    config: { url: 'https://new.example/mcp' },
+  });
+
+  assert.deepEqual(result, { status: 'conflict', reason: 'stale_edit' });
+  assert.deepEqual(order, ['transform']);
+  await controller.close();
+});
+
+test('TUI MCP import preserves unrelated external edits and rejects changed preview entries', async () => {
+  const order: string[] = [];
+  const store = mutableConfigStore(
+    { version: 3, mcpServers: { existing: { command: 'one' } } },
+    order,
+  );
+  const manager = managementManager(order);
+  const connection = connectionHarness();
+  const controller = createTuiMcpController(
+    { workspaceRoot: '/unused', connection: connection.connection },
+    { configStore: store.store, manager: manager.manager, createProvider: () => undefined },
+  );
+  await waitFor(() => controller.snapshot().initialization === 'ready');
+  const preview = controller.previewImport('{"docs":{"url":"https://docs.example/mcp"}}');
+  assert.equal(preview.status, 'ready');
+  if (preview.status !== 'ready') throw new Error('preview did not prepare');
+  store.replace({
+    version: 3,
+    mcpServers: { existing: { command: 'externally-edited' } },
+  });
+
+  assert.deepEqual(
+    await controller.execute({ kind: 'commit_import', previewId: preview.preview.previewId }),
+    {
+      status: 'applied',
+      effect: 'published',
+    },
+  );
+  const existing = (await store.store.get()).mcpServers.existing;
+  assert.ok(existing && 'command' in existing);
+  assert.equal(existing.command, 'externally-edited');
+
+  const stale = controller.previewImport('{"docs":{"url":"https://replacement.example/mcp"}}');
+  assert.equal(stale.status, 'ready');
+  if (stale.status !== 'ready') throw new Error('preview did not prepare');
+  store.replace({
+    version: 3,
+    mcpServers: {
+      ...(await store.store.get()).mcpServers,
+      docs: { url: 'https://concurrent.example/mcp' },
+    },
+  });
+  assert.deepEqual(
+    await controller.execute({ kind: 'commit_import', previewId: stale.preview.previewId }),
+    {
+      status: 'conflict',
+      reason: 'stale_import',
+    },
+  );
+  await controller.close();
+});
+
+test('TUI MCP keeps a durable mutation visible when manager synchronization fails', async () => {
+  const order: string[] = [];
+  const store = mutableConfigStore(emptyConfig(), order);
+  const manager = managementManager(order);
+  const connection = connectionHarness();
+  const controller = createTuiMcpController(
+    { workspaceRoot: '/unused', connection: connection.connection },
+    { configStore: store.store, manager: manager.manager, createProvider: () => undefined },
+  );
+  await waitFor(() => controller.snapshot().initialization === 'ready');
+  manager.failNextSync();
+
+  const result = await controller.execute({
+    kind: 'add',
+    serverId: 'local',
+    config: { command: 'server' },
+  });
+
+  assert.deepEqual(result, { status: 'applied', effect: 'sync_failed' });
+  assert.equal(controller.snapshot().configuration, 'out_of_sync');
+  assert.deepEqual(controller.snapshot().servers[0], {
+    serverId: 'local',
+    configured: true,
+    synchronized: false,
+    enabled: true,
+    configuredTransport: 'stdio',
+    configuredProtocol: 'legacy',
+    toolCount: 0,
+  });
+  assert.ok((await store.store.get()).mcpServers.local);
+  await controller.close();
+});
+
+test('TUI MCP reports a committed action as pending while the Host is unavailable', async () => {
+  const order: string[] = [];
+  const store = mutableConfigStore(emptyConfig(), order);
+  const manager = managementManager(order);
+  const connection = connectionHarness();
+  connection.emit({ kind: 'unavailable' });
+  const controller = createTuiMcpController(
+    { workspaceRoot: '/unused', connection: connection.connection },
+    { configStore: store.store, manager: manager.manager, createProvider: () => undefined },
+  );
+  await waitFor(() => controller.snapshot().initialization === 'ready');
+
+  assert.deepEqual(
+    await controller.execute({ kind: 'add', serverId: 'local', config: { command: 'server' } }),
+    { status: 'applied', effect: 'pending_host' },
+  );
+  await controller.close();
+});
+
+test('TUI MCP close fences an admitted mutation before persistence', async () => {
+  const transactionAdmission = deferred();
+  let reads = 0;
+  let transforms = 0;
+  let writes = 0;
+  const store = {
+    get: async () => {
+      reads += 1;
+      return emptyConfig();
+    },
+    transform: async (
+      apply: (current: McpConfigFile) => McpConfigFile | Promise<McpConfigFile>,
+    ) => {
+      transforms += 1;
+      await transactionAdmission.promise;
+      const next = await apply(emptyConfig());
+      writes += 1;
+      return next;
+    },
+  };
+  const manager = managementManager([]);
+  const connection = connectionHarness();
+  const controller = createTuiMcpController(
+    { workspaceRoot: '/unused', connection: connection.connection },
+    { configStore: store, manager: manager.manager, createProvider: () => undefined },
+  );
+  await waitFor(() => controller.snapshot().initialization === 'ready');
+  const executing = controller.execute({
+    kind: 'add',
+    serverId: 'late',
+    config: { command: 'server' },
+  });
+  await waitFor(() => transforms === 1);
+  const closing = controller.close();
+  transactionAdmission.resolve();
+
+  assert.deepEqual(await executing, { status: 'failed', reason: 'closed' });
+  await closing;
+  assert.equal(reads, 1);
+  assert.equal(writes, 0);
+});
+
+test('TUI MCP waits for manager synchronization before publishing an action snapshot', async () => {
+  const store = mutableConfigStore(emptyConfig(), []);
+  const actionSync = deferred();
+  let syncCount = 0;
+  let listener: (() => void) | undefined;
+  let revision = 0;
+  const manager = {
+    sync: async () => {
+      syncCount += 1;
+      revision += 1;
+      listener?.();
+      if (syncCount === 2) await actionSync.promise;
+    },
+    statuses: () => [],
+    toolSnapshot: () => ({ revision, tools: [{}] }) as unknown as McpToolSnapshot,
+    callTool: async () => ({ content: [] }),
+    test: async () => ({ ok: true, status: connectedStatus('local', 1), latencyMs: 1 }),
+    reconnect: async () => connectedStatus('local', 1),
+    forgetServerCredentials: async () => undefined,
+    onChange: (next: () => void) => {
+      listener = next;
+      return () => {
+        listener = undefined;
+      };
+    },
+    close: async () => undefined,
+  } as unknown as ReturnType<typeof managementManager>['manager'];
+  const connection = connectionHarness();
+  const controller = createTuiMcpController(
+    { workspaceRoot: '/unused', connection: connection.connection },
+    { configStore: store.store, manager, createProvider: () => provider('provider') },
+  );
+  await waitFor(() => controller.snapshot().publication === 'published');
+  connection.replacements.length = 0;
+
+  const executing = controller.execute({
+    kind: 'add',
+    serverId: 'local',
+    config: { command: 'server' },
+  });
+  await waitFor(() => syncCount === 2);
+  assert.equal(connection.replacements.length, 0);
+  assert.equal(controller.snapshot().configuration, 'synchronizing');
+  actionSync.resolve();
+
+  assert.deepEqual(await executing, { status: 'applied', effect: 'published' });
+  assert.equal(connection.replacements.length, 1);
+  await controller.close();
+});
+
+test('TUI MCP rebases an action over an unrelated concurrent config edit', async () => {
+  let config: McpConfigFile = {
+    version: 3,
+    mcpServers: { existing: { command: 'before' } },
+  };
+  const store = {
+    get: async () => structuredClone(config),
+    transform: async (
+      apply: (current: McpConfigFile) => McpConfigFile | Promise<McpConfigFile>,
+    ) => {
+      config = await apply({
+        version: 3,
+        mcpServers: { existing: { command: 'concurrent' } },
+      });
+      return structuredClone(config);
+    },
+  };
+  const manager = managementManager([]);
+  const connection = connectionHarness();
+  const controller = createTuiMcpController(
+    { workspaceRoot: '/unused', connection: connection.connection },
+    { configStore: store, manager: manager.manager, createProvider: () => undefined },
+  );
+  await waitFor(() => controller.snapshot().initialization === 'ready');
+
+  assert.deepEqual(
+    await controller.execute({ kind: 'add', serverId: 'local', config: { command: 'server' } }),
+    { status: 'applied', effect: 'published' },
+  );
+  const existing = config.mcpServers.existing;
+  assert.ok(existing && 'command' in existing);
+  assert.equal(existing.command, 'concurrent');
+  assert.ok(config.mcpServers.local);
+  await controller.close();
+});
+
+test('independent TUI controllers preserve concurrent additions in one workspace', async (t) => {
+  const root = await mkdtemp(join(tmpdir(), 'maka-tui-mcp-concurrent-'));
+  t.after(() => rm(root, { recursive: true, force: true }));
+  await createMcpConfigStore(root).transform((current) => current);
+  const left = createTuiMcpController(
+    { workspaceRoot: root, connection: connectionHarness().connection },
+    {
+      configStore: createMcpConfigStore(root),
+      manager: managementManager([]).manager,
+      createProvider: () => undefined,
+    },
+  );
+  const right = createTuiMcpController(
+    { workspaceRoot: root, connection: connectionHarness().connection },
+    {
+      configStore: createMcpConfigStore(root),
+      manager: managementManager([]).manager,
+      createProvider: () => undefined,
+    },
+  );
+  await waitFor(
+    () => left.snapshot().initialization === 'ready' && right.snapshot().initialization === 'ready',
+  );
+
+  const [leftResult, rightResult] = await Promise.all([
+    left.execute({ kind: 'add', serverId: 'left', config: { command: 'left-server' } }),
+    right.execute({ kind: 'add', serverId: 'right', config: { command: 'right-server' } }),
+  ]);
+
+  assert.equal(leftResult.status, 'applied');
+  assert.equal(rightResult.status, 'applied');
+  const saved = await createMcpConfigStore(root).get();
+  assert.ok(saved.mcpServers.left);
+  assert.ok(saved.mcpServers.right);
+  await Promise.all([left.close(), right.close()]);
+});
+
+test('same-server credential retirement stays inside the shared config transaction', async (t) => {
+  const root = await mkdtemp(join(tmpdir(), 'maka-tui-mcp-retirement-'));
+  t.after(() => rm(root, { recursive: true, force: true }));
+  await createMcpConfigStore(root).upsert('docs', { url: 'https://old.example/mcp' });
+  const retirement = deferred();
+  const leftOrder: string[] = [];
+  const rightOrder: string[] = [];
+  const left = createTuiMcpController(
+    { workspaceRoot: root, connection: connectionHarness().connection },
+    {
+      configStore: createMcpConfigStore(root),
+      manager: managementManager(leftOrder, { credentialWait: retirement.promise }).manager,
+      createProvider: () => undefined,
+    },
+  );
+  const right = createTuiMcpController(
+    { workspaceRoot: root, connection: connectionHarness().connection },
+    {
+      configStore: createMcpConfigStore(root),
+      manager: managementManager(rightOrder).manager,
+      createProvider: () => undefined,
+    },
+  );
+  await waitFor(
+    () => left.snapshot().initialization === 'ready' && right.snapshot().initialization === 'ready',
+  );
+  const leftEdit = left.configForEdit('docs');
+  const rightEdit = right.configForEdit('docs');
+  assert.ok(leftEdit);
+  assert.ok(rightEdit);
+
+  const first = left.execute({
+    kind: 'edit',
+    serverId: 'docs',
+    expectedRevision: leftEdit.revision,
+    config: { url: 'https://left.example/mcp' },
+  });
+  await waitFor(() => leftOrder.includes('forget:docs'));
+  const second = right.execute({
+    kind: 'edit',
+    serverId: 'docs',
+    expectedRevision: rightEdit.revision,
+    config: { url: 'https://right.example/mcp' },
+  });
+  for (let attempt = 0; attempt < 10; attempt += 1) {
+    await new Promise<void>((resolve) => setImmediate(resolve));
+  }
+  assert.equal(rightOrder.includes('forget:docs'), false);
+
+  retirement.resolve();
+  assert.deepEqual(await first, { status: 'applied', effect: 'published' });
+  assert.deepEqual(await second, { status: 'conflict', reason: 'stale_edit' });
+  assert.equal(rightOrder.includes('forget:docs'), false);
+  const saved = (await createMcpConfigStore(root).get()).mcpServers.docs;
+  assert.ok(saved && 'url' in saved);
+  assert.equal(saved.url, 'https://left.example/mcp');
+  await Promise.all([left.close(), right.close()]);
+});
+
+test('TUI MCP manages enabled state, tests, reconnects, and removes through one lane', async () => {
+  const order: string[] = [];
+  const store = mutableConfigStore(
+    {
+      version: 3,
+      mcpServers: { docs: { enabled: false, url: 'https://docs.example/mcp' } },
+    },
+    order,
+  );
+  const manager = managementManager(order);
+  const connection = connectionHarness();
+  const controller = createTuiMcpController(
+    { workspaceRoot: '/unused', connection: connection.connection },
+    { configStore: store.store, manager: manager.manager, createProvider: () => undefined },
+  );
+  await waitFor(() => controller.snapshot().initialization === 'ready');
+  order.length = 0;
+
+  assert.deepEqual(
+    await controller.execute({ kind: 'set_enabled', serverId: 'docs', enabled: true }),
+    { status: 'applied', effect: 'published' },
+  );
+  assert.equal((await store.store.get()).mcpServers.docs?.enabled, true);
+  const tested = await controller.execute({ kind: 'test', serverId: 'docs' });
+  assert.equal(tested.status, 'tested');
+  assert.deepEqual(await controller.execute({ kind: 'reconnect', serverId: 'docs' }), {
+    status: 'applied',
+    effect: 'published',
+  });
+  assert.deepEqual(await controller.execute({ kind: 'remove', serverId: 'docs' }), {
+    status: 'applied',
+    effect: 'published',
+  });
+  assert.deepEqual(
+    order.filter((entry) => entry.startsWith('test') || entry.startsWith('reconnect')),
+    ['test:docs', 'reconnect:docs'],
+  );
+  assert.equal((await store.store.get()).mcpServers.docs, undefined);
+  await controller.close();
+});
+
+function mutableConfigStore(initial: McpConfigFile, order: string[]) {
+  let config = structuredClone(initial);
+  const store = {
+    get: async () => {
+      order.push('get');
+      return structuredClone(config);
+    },
+    transform: async (
+      apply: (current: McpConfigFile) => McpConfigFile | Promise<McpConfigFile>,
+    ) => {
+      order.push('transform');
+      config = structuredClone(await apply(structuredClone(config)));
+      return structuredClone(config);
+    },
+  };
+  return {
+    store,
+    replace(next: McpConfigFile) {
+      config = structuredClone(next);
+    },
+  };
+}
+
+function managementManager(
+  order: string[],
+  options: { readonly credentialFailure?: boolean; readonly credentialWait?: Promise<void> } = {},
+) {
+  let listener: (() => void) | undefined;
+  let syncFailure = false;
+  let revision = 0;
+  const statuses: McpServerStatus[] = [];
+  const manager = {
+    sync: async () => {
+      order.push('sync');
+      if (syncFailure) {
+        syncFailure = false;
+        throw new Error('sync failed');
+      }
+      revision += 1;
+      listener?.();
+    },
+    statuses: () => statuses,
+    toolSnapshot: () => ({ revision, tools: [] }) as McpToolSnapshot,
+    callTool: async () => ({ content: [] }),
+    test: async (serverId: string) => {
+      order.push(`test:${serverId}`);
+      return { ok: true, status: connectedStatus(serverId, 0), latencyMs: 1 };
+    },
+    reconnect: async (serverId: string) => {
+      order.push(`reconnect:${serverId}`);
+      return connectedStatus(serverId, 0);
+    },
+    forgetServerCredentials: async (serverId: string) => {
+      order.push(`forget:${serverId}`);
+      if (options.credentialFailure) throw new Error('credential cleanup failed');
+      await options.credentialWait;
+    },
+    onChange: (next: () => void) => {
+      listener = next;
+      return () => {
+        if (listener === next) listener = undefined;
+      };
+    },
+    close: async () => undefined,
+  } as unknown as Pick<
+    McpClientManager,
+    | 'sync'
+    | 'statuses'
+    | 'toolSnapshot'
+    | 'callTool'
+    | 'test'
+    | 'reconnect'
+    | 'forgetServerCredentials'
+    | 'onChange'
+    | 'close'
+  >;
+  return {
+    manager,
+    failNextSync() {
+      syncFailure = true;
+    },
+  };
+}
+
 function managerHarness(revision: number, statuses: McpServerStatus[]) {
   let currentRevision = revision;
   let toolCount = revision === 0 ? 0 : 1;
@@ -196,6 +782,9 @@ function managerHarness(revision: number, statuses: McpServerStatus[]) {
         tools: new Array(toolCount).fill({}),
       }) as McpToolSnapshot,
     callTool: async () => ({ content: [] }),
+    test: async () => ({ ok: true, status: connectedStatus('local', 1), latencyMs: 1 }),
+    reconnect: async () => connectedStatus('local', 1),
+    forgetServerCredentials: async () => undefined,
     onChange: (next: () => void) => {
       listener = next;
       return () => {
@@ -207,7 +796,15 @@ function managerHarness(revision: number, statuses: McpServerStatus[]) {
     },
   } as unknown as Pick<
     McpClientManager,
-    'sync' | 'statuses' | 'toolSnapshot' | 'callTool' | 'onChange' | 'close'
+    | 'sync'
+    | 'statuses'
+    | 'toolSnapshot'
+    | 'callTool'
+    | 'onChange'
+    | 'test'
+    | 'reconnect'
+    | 'forgetServerCredentials'
+    | 'close'
   >;
   return {
     manager,
@@ -290,6 +887,14 @@ function emptyConfig(): McpConfigFile {
   return { version: 3, mcpServers: {} };
 }
 
+function configStoreHarness(get: () => Promise<McpConfigFile>) {
+  return {
+    get,
+    transform: async (apply: (current: McpConfigFile) => McpConfigFile | Promise<McpConfigFile>) =>
+      apply(await get()),
+  };
+}
+
 async function waitFor(condition: () => boolean): Promise<void> {
   for (let attempt = 0; attempt < 1_000 && !condition(); attempt += 1) {
     await new Promise<void>((resolve) => setImmediate(resolve));
diff --git a/packages/cli/src/pi-tui-mcp-status.ts b/packages/cli/src/pi-tui-mcp-status.ts
index 105a8b0a8..a4f967eb7 100644
--- a/packages/cli/src/pi-tui-mcp-status.ts
+++ b/packages/cli/src/pi-tui-mcp-status.ts
@@ -18,28 +18,90 @@
  */
 
 import {
+  Editor,
   Key,
   matchesKey,
   truncateToWidth,
   visibleWidth,
   type Component,
+  type TUI,
 } from '@earendil-works/pi-tui';
+import type { McpProtocolPreference, McpServerConfig } from '@maka/core/mcp';
 import type { UiLocale } from '@maka/core/ui-locale';
-import type { TuiMcpServerSnapshot, TuiMcpSurface } from './tui-mcp-control.js';
-import { ansi } from './tui-ansi.js';
+import { normalizeMcpConfig } from '@maka/storage/mcp-config-store';
+import type {
+  TuiMcpAction,
+  TuiMcpActionResult,
+  TuiMcpImportPreview,
+  TuiMcpManagement,
+  TuiMcpServerSnapshot,
+} from './tui-mcp-control.js';
+import { ansi, editorTheme } from './tui-ansi.js';
 
 const CHROME_ROWS = 2;
 
-export class McpStatusOverlay implements Component {
+type GuidedDraft = {
+  serverId: string;
+  transport?: 'stdio' | 'remote';
+  command?: string;
+  args?: string[];
+  url?: string;
+  cwd?: string;
+  env?: Record<string, string>;
+  headers?: Record<string, string>;
+  protocol?: McpProtocolPreference;
+};
+
+type InputKind =
+  | 'server_id'
+  | 'command'
+  | 'args'
+  | 'url'
+  | 'cwd'
+  | 'env'
+  | 'headers'
+  | 'edit'
+  | 'import';
+
+type McpOverlayPhase =
+  | { kind: 'list' }
+  | { kind: 'add_choice' }
+  | { kind: 'transport'; draft: GuidedDraft }
+  | { kind: 'protocol'; draft: GuidedDraft }
+  | {
+      kind: 'input';
+      input: InputKind;
+      draft?: GuidedDraft;
+      serverId?: string;
+      revision?: string;
+    }
+  | { kind: 'confirm_add'; draft: GuidedDraft }
+  | { kind: 'confirm_import'; preview: TuiMcpImportPreview }
+  | { kind: 'confirm_remove'; serverId: string }
+  | { kind: 'busy'; label: string };
+
+/** One in-frame state machine for status, editing, confirmation, and errors.
+ * Raw config values live only in the editor and are cleared on every exit;
+ * no management result is written into the conversation transcript. */
+export class McpManagementOverlay implements Component {
   private top = 0;
   private documentRows = 0;
   private bodyRows = 0;
+  private selected = 0;
+  private serverRows: { start: number; end: number }[] = [];
+  private phase: McpOverlayPhase = { kind: 'list' };
+  private notice: { level: 'info' | 'error'; text: string } | undefined;
   private readonly dispose: () => void;
+  private editor: Editor | undefined;
+  private closed = false;
+  private actionAttempt = 0;
 
   constructor(
     private readonly input: {
       readonly locale: UiLocale;
-      readonly surface?: TuiMcpSurface;
+      readonly tui?: TUI;
+      readonly surface?: TuiMcpManagement;
+      canManage?(): boolean;
       viewportRows(): number;
       onClose(): void;
       onChange(): void;
@@ -48,20 +110,53 @@ export class McpStatusOverlay implements Component {
     this.dispose = input.surface?.subscribe(input.onChange) ?? (() => undefined);
   }
 
-  invalidate(): void {}
+  invalidate(): void {
+    this.editor?.invalidate();
+  }
 
   handleInput(data: string): void {
+    if (this.phase.kind === 'input') {
+      if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl('c'))) {
+        this.backToList();
+      } else {
+        this.editor?.handleInput(data);
+      }
+      return;
+    }
+    if (this.phase.kind === 'busy') {
+      if (matchesKey(data, Key.escape)) {
+        this.actionAttempt += 1;
+        this.backToList();
+      } else if (matchesKey(data, 'q')) {
+        this.actionAttempt += 1;
+        this.close();
+      }
+      return;
+    }
     if (matchesKey(data, Key.escape) || matchesKey(data, 'q')) {
-      this.dispose();
-      this.input.onClose();
+      if (this.phase.kind === 'list') this.close();
+      else this.backToList();
       return;
     }
-    if (matchesKey(data, Key.up)) this.scrollBy(-1);
-    else if (matchesKey(data, Key.down)) this.scrollBy(1);
-    else if (matchesKey(data, Key.pageUp)) this.scrollBy(-Math.max(1, this.bodyRows));
-    else if (matchesKey(data, Key.pageDown)) this.scrollBy(Math.max(1, this.bodyRows));
-    else if (matchesKey(data, Key.home)) this.scrollTo(0);
-    else if (matchesKey(data, Key.end)) this.scrollTo(this.maxTop());
+    if (this.phase.kind === 'list') this.handleListInput(data);
+    else if (this.phase.kind === 'add_choice') this.handleAddChoice(data);
+    else if (this.phase.kind === 'transport') this.handleTransport(data);
+    else if (this.phase.kind === 'protocol') this.handleProtocol(data);
+    else if (this.phase.kind === 'confirm_add' && matchesKey(data, 'y')) {
+      try {
+        const config = guidedConfig(this.phase.draft);
+        if (config) {
+          void this.runAction({ kind: 'add', serverId: this.phase.draft.serverId, config });
+        }
+      } catch {
+        this.notice = { level: 'error', text: copy(this.input.locale, 'invalid') };
+        this.backToList(false);
+      }
+    } else if (this.phase.kind === 'confirm_import' && matchesKey(data, 'y')) {
+      void this.runAction({ kind: 'commit_import', previewId: this.phase.preview.previewId });
+    } else if (this.phase.kind === 'confirm_remove' && matchesKey(data, 'y')) {
+      void this.runAction({ kind: 'remove', serverId: this.phase.serverId });
+    }
   }
 
   render(width: number): string[] {
@@ -69,8 +164,9 @@ export class McpStatusOverlay implements Component {
     const viewportRows = Math.max(1, Math.floor(this.input.viewportRows()));
     const showFooter = viewportRows > 2;
     this.bodyRows = Math.max(0, viewportRows - (showFooter ? CHROME_ROWS : 1));
-    const document = this.document();
+    const document = this.document(safeWidth);
     this.documentRows = document.length;
+    this.keepSelectionVisible();
     this.top = clamp(this.top, 0, this.maxTop());
     const visible = document.slice(this.top, this.top + this.bodyRows);
     const start = visible.length === 0 ? 0 : this.top + 1;
@@ -87,72 +183,402 @@ export class McpStatusOverlay implements Component {
       ),
     ];
     if (!showFooter) return [header, ...body];
-    const footer =
-      this.input.locale === 'zh'
-        ? '↑/↓ 滚动 · PgUp/PgDn 翻页 · Home/End 跳转 · q/Esc 关闭'
-        : '↑/↓ scroll · PgUp/PgDn page · Home/End jump · q/Esc close';
-    return [header, ...body, padLine(ansi.dim(footer), safeWidth)];
+    return [header, ...body, padLine(ansi.dim(this.footer()), safeWidth)];
+  }
+
+  private management(): TuiMcpManagement | undefined {
+    return this.input.surface;
+  }
+
+  private handleListInput(data: string): void {
+    const servers = this.input.surface?.snapshot().servers ?? [];
+    if (matchesKey(data, Key.up)) {
+      this.selected = clamp(this.selected - 1, 0, servers.length - 1);
+    } else if (matchesKey(data, Key.down)) {
+      this.selected = clamp(this.selected + 1, 0, servers.length - 1);
+    } else if (matchesKey(data, Key.pageUp)) {
+      this.moveSelectionByPage(-1, servers.length);
+    } else if (matchesKey(data, Key.pageDown)) {
+      this.moveSelectionByPage(1, servers.length);
+    } else if (matchesKey(data, Key.home)) this.selected = 0;
+    else if (matchesKey(data, Key.end)) this.selected = Math.max(0, servers.length - 1);
+    else if (matchesKey(data, 'a') && this.management()) this.phase = { kind: 'add_choice' };
+    else {
+      const server = servers[this.selected];
+      if (!server || !this.management()) return;
+      if (matchesKey(data, Key.enter)) this.startEdit(server.serverId);
+      else if (matchesKey(data, Key.space)) {
+        void this.runAction({
+          kind: 'set_enabled',
+          serverId: server.serverId,
+          enabled: !server.enabled,
+        });
+      } else if (matchesKey(data, 't')) {
+        void this.runAction({ kind: 'test', serverId: server.serverId });
+      } else if (matchesKey(data, 'r')) {
+        void this.runAction({ kind: 'reconnect', serverId: server.serverId });
+      } else if (matchesKey(data, 'd') && server.configured) {
+        this.phase = { kind: 'confirm_remove', serverId: server.serverId };
+      }
+    }
+    this.input.onChange();
+  }
+
+  private handleAddChoice(data: string): void {
+    if (matchesKey(data, 'g')) this.startInput('server_id', { serverId: '' });
+    else if (matchesKey(data, 'j')) this.startInput('import');
+  }
+
+  private handleTransport(data: string): void {
+    if (this.phase.kind !== 'transport') return;
+    if (matchesKey(data, '1')) {
+      this.phase.draft.transport = 'stdio';
+      this.startInput('command', this.phase.draft);
+    } else if (matchesKey(data, '2')) {
+      this.phase.draft.transport = 'remote';
+      this.startInput('url', this.phase.draft);
+    }
+  }
+
+  private handleProtocol(data: string): void {
+    if (this.phase.kind !== 'protocol') return;
+    const protocol = matchesKey(data, '1')
+      ? 'legacy'
+      : matchesKey(data, '2')
+        ? 'auto'
+        : matchesKey(data, '3')
+          ? '2026-07-28'
+          : undefined;
+    if (!protocol) return;
+    this.phase.draft.protocol = protocol;
+    this.startInput(this.phase.draft.transport === 'stdio' ? 'cwd' : 'headers', this.phase.draft);
+  }
+
+  private startEdit(serverId: string): void {
+    const edit = this.management()?.configForEdit(serverId);
+    if (!edit) {
+      this.notice = { level: 'error', text: copy(this.input.locale, 'missing') };
+      return;
+    }
+    this.startInput(
+      'edit',
+      undefined,
+      serverId,
+      edit.revision,
+      JSON.stringify(edit.config, null, 2),
+    );
+  }
+
+  private startInput(
+    input: InputKind,
+    draft?: GuidedDraft,
+    serverId?: string,
+    revision?: string,
+    value = '',
+  ): void {
+    if (!this.input.tui) return;
+    this.clearEditor();
+    this.notice = undefined;
+    this.phase = { kind: 'input', input, draft, serverId, revision };
+    this.editor = new Editor(this.input.tui, editorTheme(), { paddingX: 0 });
+    this.editor.onSubmit = (submitted) => this.submitInput(submitted);
+    this.editor.setText(value);
+    this.editor.focused = true;
+    this.input.onChange();
   }
 
-  private document(): string[] {
+  private submitInput(value: string): void {
+    if (this.phase.kind !== 'input') return;
+    const phase = this.phase;
+    const trimmed = value.trim();
+    try {
+      if (phase.input === 'server_id') {
+        if (!trimmed) throw new Error();
+        const draft = phase.draft ?? { serverId: '' };
+        draft.serverId = trimmed;
+        this.clearEditor();
+        this.phase = { kind: 'transport', draft };
+      } else if (phase.input === 'command') {
+        if (!trimmed || !phase.draft) throw new Error();
+        phase.draft.command = trimmed;
+        this.startInput('args', phase.draft);
+      } else if (phase.input === 'args') {
+        if (!phase.draft) throw new Error();
+        phase.draft.args = trimmed ? stringArray(trimmed) : undefined;
+        this.clearEditor();
+        this.phase = { kind: 'protocol', draft: phase.draft };
+      } else if (phase.input === 'url') {
+        if (!trimmed || !phase.draft) throw new Error();
+        phase.draft.url = trimmed;
+        this.clearEditor();
+        this.phase = { kind: 'protocol', draft: phase.draft };
+      } else if (phase.input === 'cwd') {
+        if (!phase.draft) throw new Error();
+        phase.draft.cwd = trimmed || undefined;
+        this.startInput('env', phase.draft);
+      } else if (phase.input === 'env') {
+        if (!phase.draft) throw new Error();
+        phase.draft.env = trimmed ? stringMap(trimmed) : undefined;
+        this.clearEditor();
+        this.phase = { kind: 'confirm_add', draft: phase.draft };
+      } else if (phase.input === 'headers') {
+        if (!phase.draft) throw new Error();
+        phase.draft.headers = trimmed ? stringMap(trimmed) : undefined;
+        this.clearEditor();
+        this.phase = { kind: 'confirm_add', draft: phase.draft };
+      } else if (phase.input === 'edit') {
+        if (!phase.serverId || !phase.revision) throw new Error();
+        const config = normalizeOneServer(phase.serverId, trimmed);
+        this.clearEditor();
+        void this.runAction({
+          kind: 'edit',
+          serverId: phase.serverId,
+          expectedRevision: phase.revision,
+          config,
+        });
+      } else {
+        const preview = this.management()?.previewImport(value);
+        if (!preview || preview.status !== 'ready') throw new Error();
+        this.clearEditor();
+        this.phase = { kind: 'confirm_import', preview: preview.preview };
+      }
+      this.notice = undefined;
+    } catch {
+      this.notice = { level: 'error', text: copy(this.input.locale, 'invalid') };
+    }
+    this.input.onChange();
+  }
+
+  private async runAction(action: TuiMcpAction): Promise<void> {
+    const management = this.management();
+    if (!management || this.phase.kind === 'busy') return;
+    if (this.input.canManage && !this.input.canManage()) {
+      this.backToList(false);
+      this.notice = { level: 'error', text: copy(this.input.locale, 'turn_active') };
+      this.input.onChange();
+      return;
+    }
+    this.clearEditor();
+    const attempt = ++this.actionAttempt;
+    this.phase = { kind: 'busy', label: actionLabel(action, this.input.locale) };
+    this.input.onChange();
+    let result: TuiMcpActionResult;
+    try {
+      result = await management.execute(action);
+    } catch {
+      result = { status: 'failed', reason: 'manager-failed' };
+    }
+    if (this.closed || attempt !== this.actionAttempt) return;
+    this.phase = { kind: 'list' };
+    this.notice = actionNotice(result, this.input.locale);
+    this.input.onChange();
+  }
+
+  private document(width: number): string[] {
+    this.serverRows = [];
     const snapshot = this.input.surface?.snapshot();
-    if (!snapshot) {
-      return this.input.locale === 'zh'
-        ? [
-            ansi.yellow('当前 TUI 未连接本地 MCP 控制面。'),
-            '远程 Runtime Host 的客户端 MCP 工具关联将在后续版本提供。',
-          ]
-        : [
-            ansi.yellow('This TUI is not connected to a local MCP control plane.'),
-            'Client MCP tool association for remote Runtime Hosts is planned for a later release.',
-          ];
+    if (!snapshot) return unavailableDocument(this.input.locale);
+    if (this.phase.kind === 'input') return this.inputDocument(width);
+    if (this.phase.kind === 'add_choice') {
+      return [
+        heading(this.input.locale, 'Add MCP server', '添加 MCP 服务器'),
+        '',
+        'g  Guided setup',
+        'j  Paste JSON',
+      ];
+    }
+    if (this.phase.kind === 'transport') {
+      return [
+        heading(this.input.locale, 'Transport', '传输方式'),
+        '',
+        '1  stdio',
+        '2  Streamable HTTP',
+      ];
+    }
+    if (this.phase.kind === 'protocol') {
+      return [
+        heading(this.input.locale, 'Protocol preference', '协议偏好'),
+        '',
+        '1  legacy',
+        '2  auto',
+        '3  2026-07-28',
+      ];
+    }
+    if (this.phase.kind === 'confirm_add') {
+      return confirmAddDocument(this.phase.draft, this.input.locale);
+    }
+    if (this.phase.kind === 'confirm_import') {
+      return confirmImportDocument(this.phase.preview, this.input.locale);
+    }
+    if (this.phase.kind === 'confirm_remove') {
+      return [
+        ansi.red(
+          heading(
+            this.input.locale,
+            `Remove ${this.phase.serverId}?`,
+            `删除 ${this.phase.serverId}?`,
+          ),
+        ),
+        '',
+        confirmCopy(this.input.locale),
+      ];
     }
+    if (this.phase.kind === 'busy') return [ansi.yellow(this.phase.label)];
     const lines = [publicationLine(snapshot, this.input.locale)];
-    if (snapshot.initialization === 'loading') {
-      lines.push(
-        this.input.locale === 'zh'
-          ? '正在读取 mcp.json 并发现工具…'
-          : 'Loading mcp.json and discovering tools…',
-      );
-      return lines;
+    if (snapshot.configuration !== 'ready') {
+      lines.push(configurationLine(snapshot.configuration, this.input.locale));
     }
-    if (snapshot.initialization === 'error') {
+    if (this.notice) {
       lines.push(
-        this.input.locale === 'zh'
-          ? ansi.red('无法读取或应用 MCP 配置;没有向 Runtime Host 发布工具。')
-          : ansi.red(
-              'MCP configuration could not be loaded; no tools were published to the Runtime Host.',
-            ),
+        this.notice.level === 'error' ? ansi.red(this.notice.text) : ansi.green(this.notice.text),
       );
-      return lines;
     }
-    if (snapshot.servers.length === 0) {
-      lines.push(
-        this.input.locale === 'zh' ? '尚未配置 MCP 服务器。' : 'No MCP servers are configured.',
-      );
-      return lines;
+    if (snapshot.initialization === 'loading') return [...lines, loadingCopy(this.input.locale)];
+    if (snapshot.initialization === 'error') {
+      return [...lines, ansi.red(loadErrorCopy(this.input.locale))];
     }
+    if (snapshot.servers.length === 0) return [...lines, '', emptyCopy(this.input.locale)];
     lines.push('');
-    for (const server of snapshot.servers) lines.push(...serverLines(server, this.input.locale));
+    this.selected = clamp(this.selected, 0, snapshot.servers.length - 1);
+    snapshot.servers.forEach((server, index) => {
+      const rows = serverLines(server, this.input.locale, index === this.selected);
+      const start = lines.length;
+      lines.push(...rows);
+      this.serverRows.push({ start, end: lines.length - 1 });
+    });
     return lines;
   }
 
-  private scrollBy(delta: number): void {
-    this.scrollTo(this.top + delta);
+  private inputDocument(width: number): string[] {
+    if (this.phase.kind !== 'input' || !this.editor) return [];
+    const label = inputLabel(this.phase.input, this.input.locale);
+    const hint = inputHint(this.phase.input, this.input.locale);
+    return [
+      ansi.bold(label),
+      ...(hint ? [ansi.dim(hint)] : []),
+      '',
+      ...this.editor.render(Math.max(1, width - 2)),
+      ...(this.notice ? [ansi.red(this.notice.text)] : []),
+    ];
   }
 
-  private scrollTo(next: number): void {
-    this.top = clamp(next, 0, this.maxTop());
+  private footer(): string {
+    if (this.phase.kind !== 'list') return this.input.locale === 'zh' ? 'Esc 返回' : 'Esc back';
+    if (!this.management()) {
+      return this.input.locale === 'zh' ? '↑/↓ 滚动 · q/Esc 关闭' : '↑/↓ scroll · q/Esc close';
+    }
+    return this.input.locale === 'zh'
+      ? 'a 添加 · Enter 编辑 · Space 启用/停用 · t 测试 · r 重连 · d 删除 · Esc 关闭'
+      : 'a Add · Enter Edit · Space Enable/disable · t Test · r Reconnect · d Remove · Esc Close';
+  }
+
+  private backToList(clearNotice = true): void {
+    if (this.phase.kind === 'confirm_import') {
+      this.management()?.discardImportPreview(this.phase.preview.previewId);
+    }
+    this.clearEditor();
+    this.phase = { kind: 'list' };
+    if (clearNotice) this.notice = undefined;
     this.input.onChange();
   }
 
+  private clearEditor(): void {
+    if (!this.editor) return;
+    this.editor.focused = false;
+    this.editor.onSubmit = undefined;
+    this.editor.onChange = undefined;
+    this.editor = undefined;
+  }
+
+  private close(): void {
+    if (this.closed) return;
+    this.closed = true;
+    if (this.phase.kind === 'confirm_import') {
+      this.management()?.discardImportPreview(this.phase.preview.previewId);
+    }
+    this.clearEditor();
+    this.dispose();
+    this.input.onClose();
+  }
+
+  private keepSelectionVisible(): void {
+    const rows = this.serverRows[this.selected];
+    if (!rows || this.bodyRows <= 0) return;
+    if (rows.end - rows.start + 1 > this.bodyRows || rows.start < this.top) {
+      this.top = rows.start;
+    } else if (rows.end >= this.top + this.bodyRows) {
+      this.top = rows.end - this.bodyRows + 1;
+    }
+  }
+
+  private moveSelectionByPage(direction: -1 | 1, serverCount: number): void {
+    if (serverCount === 0) {
+      this.selected = 0;
+      return;
+    }
+    const current = this.serverRows[this.selected];
+    if (!current || this.serverRows.length !== serverCount) {
+      this.selected = clamp(
+        this.selected + direction * Math.max(1, this.bodyRows),
+        0,
+        serverCount - 1,
+      );
+      return;
+    }
+    const targetRow = current.start + direction * Math.max(1, this.bodyRows);
+    const target = this.serverRows.findIndex(
+      (rows) => targetRow >= rows.start && targetRow <= rows.end,
+    );
+    this.selected = target < 0 ? (direction < 0 ? 0 : Math.max(0, serverCount - 1)) : target;
+  }
+
   private maxTop(): number {
     return Math.max(0, this.documentRows - this.bodyRows);
   }
 }
 
+function normalizeOneServer(serverId: string, source: string): McpServerConfig {
+  const value: unknown = JSON.parse(source);
+  return normalizeMcpConfig({ version: 3, mcpServers: { [serverId]: value } }).mcpServers[serverId];
+}
+
+function guidedConfig(draft: GuidedDraft): McpServerConfig | undefined {
+  if (!draft.transport || !draft.protocol) return undefined;
+  const raw =
+    draft.transport === 'stdio'
+      ? {
+          command: draft.command,
+          args: draft.args,
+          cwd: draft.cwd,
+          env: draft.env,
+          protocol: draft.protocol,
+        }
+      : {
+          url: draft.url,
+          transport: 'auto',
+          headers: draft.headers,
+          protocol: draft.protocol,
+        };
+  return normalizeMcpConfig({ version: 3, mcpServers: { [draft.serverId]: raw } }).mcpServers[
+    draft.serverId
+  ];
+}
+
+function stringArray(source: string): string[] {
+  const value: unknown = JSON.parse(source);
+  if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) throw new Error();
+  return value;
+}
+
+function stringMap(source: string): Record<string, string> {
+  const value: unknown = JSON.parse(source);
+  if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error();
+  if (Object.values(value).some((entry) => typeof entry !== 'string')) throw new Error();
+  return value as Record<string, string>;
+}
+
 function publicationLine(
-  snapshot: ReturnType<TuiMcpSurface['snapshot']>,
+  snapshot: ReturnType<TuiMcpManagement['snapshot']>,
   locale: UiLocale,
 ): string {
   const publication = {
@@ -167,20 +593,204 @@ function publicationLine(
   return `${ansi.bold(publication)} · ${tools}`;
 }
 
-function serverLines(server: TuiMcpServerSnapshot, locale: UiLocale): string[] {
+function serverLines(server: TuiMcpServerSnapshot, locale: UiLocale, selected: boolean): string[] {
   const protocol = server.negotiatedProtocol
     ? `${server.negotiatedProtocol.era} ${server.negotiatedProtocol.revision}`
-    : undefined;
+    : server.configuredProtocol;
+  const transport = server.transport ?? server.configuredTransport;
   const tools = locale === 'zh' ? `${server.toolCount} 个工具` : `${server.toolCount} tools`;
-  const details = [stateLabel(server.state, locale), server.transport, protocol, tools]
+  const sync = !server.synchronized
+    ? locale === 'zh'
+      ? '配置待同步'
+      : 'config pending'
+    : undefined;
+  const details = [stateLabel(server.state, locale), transport, protocol, tools, sync]
     .filter(Boolean)
     .join(' · ');
+  const cursor = selected ? ansi.accent('›') : ' ';
   return [
-    `${statusMarker(server.state)} ${ansi.bold(server.serverId)}  ${details}`,
-    ...(server.error ? [`  ${ansi.red(server.error)}`] : []),
+    `${cursor} ${statusMarker(server.state)} ${ansi.bold(server.serverId)}  ${details}`,
+    ...(server.error ? [`    ${ansi.red(server.error)}`] : []),
   ];
 }
 
+function actionNotice(
+  result: TuiMcpActionResult,
+  locale: UiLocale,
+): { level: 'info' | 'error'; text: string } {
+  if (result.status === 'conflict') return { level: 'error', text: copy(locale, result.reason) };
+  if (result.status === 'failed') return { level: 'error', text: copy(locale, result.reason) };
+  if (result.status === 'tested') {
+    if (result.test.ok && result.effect === 'publication_failed') {
+      return { level: 'error', text: copy(locale, 'test_publication_failed') };
+    }
+    if (result.test.ok && result.effect === 'pending_host') {
+      return { level: 'info', text: copy(locale, 'test_pending_host') };
+    }
+    return {
+      level: result.test.ok ? 'info' : 'error',
+      text: result.test.ok ? copy(locale, 'test_ok') : copy(locale, 'test_failed'),
+    };
+  }
+  return {
+    level:
+      result.effect === 'sync_failed' || result.effect === 'publication_failed' ? 'error' : 'info',
+    text: copy(locale, result.effect),
+  };
+}
+
+function copy(locale: UiLocale, key: string): string {
+  const en: Record<string, string> = {
+    exists: 'That server ID already exists.',
+    stale_config: 'MCP configuration changed; retry the action.',
+    stale_edit: 'This server changed; reopen it before editing.',
+    stale_import: 'An imported entry changed; preview the import again.',
+    missing: 'That server no longer exists.',
+    closed: 'The MCP controller is closed.',
+    'invalid-config': 'The server configuration is invalid.',
+    'credential-cleanup-failed':
+      'Stored credentials could not be removed; the configuration was not changed.',
+    'persist-failed': 'The configuration could not be saved.',
+    'manager-failed': 'The MCP connection action failed.',
+    turn_active: 'MCP cannot be changed while a turn or another control action is running.',
+    invalid: 'Check the value and try again.',
+    published: 'Configuration saved and tools refreshed.',
+    pending_host: 'Configuration saved; publication is waiting for Runtime Host.',
+    sync_failed: 'Configuration saved, but the MCP manager is out of sync.',
+    publication_failed: 'Configuration saved, but capability publication failed.',
+    test_ok: 'Connection test passed.',
+    test_failed: 'Connection test failed.',
+    test_publication_failed: 'Connection test passed, but capability publication failed.',
+    test_pending_host: 'Connection test passed; publication is waiting for Runtime Host.',
+  };
+  const zh: Record<string, string> = {
+    exists: '该服务器 ID 已存在。',
+    stale_config: 'MCP 配置已变化,请重试。',
+    stale_edit: '该服务器已变化,请重新打开后编辑。',
+    stale_import: '导入项已变化,请重新预览。',
+    missing: '该服务器已不存在。',
+    closed: 'MCP 控制器已关闭。',
+    'invalid-config': '服务器配置无效。',
+    'credential-cleanup-failed': '无法删除旧凭据,配置未修改。',
+    'persist-failed': '无法保存配置。',
+    'manager-failed': 'MCP 连接操作失败。',
+    turn_active: 'Turn 或其他控制操作运行期间不能修改 MCP。',
+    invalid: '请检查输入后重试。',
+    published: '配置已保存,工具已刷新。',
+    pending_host: '配置已保存,正等待 Runtime Host 发布。',
+    sync_failed: '配置已保存,但 MCP Manager 尚未同步。',
+    publication_failed: '配置已保存,但 capability 发布失败。',
+    test_ok: '连接测试通过。',
+    test_failed: '连接测试失败。',
+    test_publication_failed: '连接测试通过,但 capability 发布失败。',
+    test_pending_host: '连接测试通过,正等待 Runtime Host 发布。',
+  };
+  return (locale === 'zh' ? zh : en)[key] ?? key;
+}
+
+function confirmAddDocument(draft: GuidedDraft, locale: UiLocale): string[] {
+  return [
+    heading(locale, 'Add this MCP server?', '添加该 MCP 服务器?'),
+    '',
+    `${draft.serverId} · ${draft.transport} · ${draft.protocol}`,
+    draft.transport === 'stdio' ? (draft.command ?? '') : (draft.url ?? ''),
+    '',
+    confirmCopy(locale),
+  ];
+}
+
+function confirmImportDocument(preview: TuiMcpImportPreview, locale: UiLocale): string[] {
+  return [
+    heading(locale, 'Import MCP servers?', '导入 MCP 服务器?'),
+    '',
+    ...preview.entries.map(
+      (entry) =>
+        `${entry.change === 'add' ? '+' : '~'} ${entry.serverId} · ${entry.transport} · ${entry.protocol}`,
+    ),
+    '',
+    confirmCopy(locale),
+  ];
+}
+
+function actionLabel(action: TuiMcpAction, locale: UiLocale): string {
+  if (locale === 'zh') return '正在应用 MCP 更改…';
+  if (action.kind === 'test') return 'Testing MCP server…';
+  if (action.kind === 'reconnect') return 'Reconnecting MCP server…';
+  return 'Applying MCP configuration…';
+}
+
+function inputLabel(kind: InputKind, locale: UiLocale): string {
+  const labels: Record<InputKind, [string, string]> = {
+    server_id: ['Server ID', '服务器 ID'],
+    command: ['Command', '命令'],
+    args: ['Arguments', '参数'],
+    url: ['Streamable HTTP URL', 'Streamable HTTP URL'],
+    cwd: ['Working directory', '工作目录'],
+    env: ['Environment', '环境变量'],
+    headers: ['Request headers', '请求头'],
+    edit: ['Edit server JSON', '编辑服务器 JSON'],
+    import: ['Paste MCP JSON', '粘贴 MCP JSON'],
+  };
+  return labels[kind][locale === 'zh' ? 1 : 0];
+}
+
+function inputHint(kind: InputKind, locale: UiLocale): string {
+  const optional = locale === 'zh' ? '可留空' : 'optional';
+  if (kind === 'args') return `JSON string array, ${optional}`;
+  if (kind === 'env' || kind === 'headers') return `JSON string map, ${optional}`;
+  if (kind === 'cwd') return optional;
+  return locale === 'zh' ? 'Enter 提交 · Esc 返回' : 'Enter submit · Esc back';
+}
+
+function configurationLine(state: 'synchronizing' | 'out_of_sync', locale: UiLocale): string {
+  if (state === 'synchronizing') {
+    return locale === 'zh' ? '配置同步中…' : 'Configuration is synchronizing…';
+  }
+  return ansi.red(
+    locale === 'zh'
+      ? '持久化配置与 MCP Manager 尚未同步。'
+      : 'Durable configuration and MCP Manager are out of sync.',
+  );
+}
+
+function unavailableDocument(locale: UiLocale): string[] {
+  return locale === 'zh'
+    ? [
+        ansi.yellow('当前 TUI 未连接本地 MCP 控制面。'),
+        '远程 Runtime Host 的客户端 MCP 工具关联将在后续版本提供。',
+      ]
+    : [
+        ansi.yellow('This TUI is not connected to a local MCP control plane.'),
+        'Client MCP tool association for remote Runtime Hosts is planned for a later release.',
+      ];
+}
+
+function heading(locale: UiLocale, en: string, zh: string): string {
+  return ansi.bold(locale === 'zh' ? zh : en);
+}
+
+function confirmCopy(locale: UiLocale): string {
+  return locale === 'zh' ? 'y 确认 · Esc 取消' : 'y Confirm · Esc cancel';
+}
+
+function loadingCopy(locale: UiLocale): string {
+  return locale === 'zh'
+    ? '正在读取 mcp.json 并发现工具…'
+    : 'Loading mcp.json and discovering tools…';
+}
+
+function loadErrorCopy(locale: UiLocale): string {
+  return locale === 'zh'
+    ? '无法读取或应用 MCP 配置;没有向 Runtime Host 发布工具。'
+    : 'MCP configuration could not be loaded; no tools were published to the Runtime Host.';
+}
+
+function emptyCopy(locale: UiLocale): string {
+  return locale === 'zh'
+    ? '尚未配置 MCP 服务器。按 a 添加。'
+    : 'No MCP servers are configured. Press a to add one.';
+}
+
 function statusMarker(state: TuiMcpServerSnapshot['state']): string {
   if (state === 'connected') return ansi.green('●');
   if (state === 'connecting') return ansi.yellow('●');
@@ -189,6 +799,7 @@ function statusMarker(state: TuiMcpServerSnapshot['state']): string {
 }
 
 function stateLabel(state: TuiMcpServerSnapshot['state'], locale: UiLocale): string {
+  if (!state) return locale === 'zh' ? '仅已配置' : 'configured only';
   if (locale === 'en') return state;
   return {
     disabled: '已停用',
diff --git a/packages/cli/src/pi-tui-runner.ts b/packages/cli/src/pi-tui-runner.ts
index fa2cb3081..ff9974a79 100644
--- a/packages/cli/src/pi-tui-runner.ts
+++ b/packages/cli/src/pi-tui-runner.ts
@@ -122,8 +122,8 @@ import { runMakaPiTuiTurn, type MakaPiTuiTurnRequest } from './pi-tui-turn.js';
 import { editorTheme, selectListTheme } from './tui-ansi.js';
 import { MakaAutocompleteAboveEditorComponent } from './tui-autocomplete-layout.js';
 import { TranscriptViewerOverlay } from './pi-tui-transcript-viewer.js';
-import { McpStatusOverlay } from './pi-tui-mcp-status.js';
-import type { TuiMcpSurface } from './tui-mcp-control.js';
+import { McpManagementOverlay } from './pi-tui-mcp-status.js';
+import type { TuiMcpManagement } from './tui-mcp-control.js';
 import { createShellRunElapsedTicker } from './shell-run-elapsed-ticker.js';
 import { createShellRunHydrationController } from './shell-run-hydration.js';
 import { sessionStatusBadge } from './tui-session-status.js';
@@ -230,8 +230,8 @@ export interface MakaPiTuiInput {
    *  whose listProviders/verify/save calls persist the connection + curated models
    *  via the host-owned stores. */
   onboarding?: MakaOnboardingSurface;
-  /** Client-owned MCP status and publication projection. Local TUI only in PR1. */
-  mcp?: TuiMcpSurface;
+  /** Client-owned MCP management and publication surface. Local TUI only. */
+  mcp?: TuiMcpManagement;
   /** First-run mode: auto-open the onboarding wizard on launch instead of
    *  waiting for /setup (used when the CLI starts with no configured connection). */
   firstRun?: boolean;
@@ -2652,9 +2652,11 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> {
 
   const showMcpStatus = (): void => {
     let overlay: OverlayHandle | undefined;
-    const viewer = new McpStatusOverlay({
+    const viewer = new McpManagementOverlay({
       locale,
+      tui,
       ...(input.mcp ? { surface: input.mcp } : {}),
+      canManage: () => !busy,
       viewportRows: () => terminal.rows,
       onClose: () => overlay?.hide(),
       onChange: () => tui.requestRender(),
diff --git a/packages/cli/src/runtime-host-tui-context.ts b/packages/cli/src/runtime-host-tui-context.ts
index 32a6850e5..88452e568 100644
--- a/packages/cli/src/runtime-host-tui-context.ts
+++ b/packages/cli/src/runtime-host-tui-context.ts
@@ -64,7 +64,7 @@ import {
 import {
   createTuiMcpController,
   type TuiMcpController,
-  type TuiMcpSurface,
+  type TuiMcpManagement,
 } from './tui-mcp-control.js';
 
 export interface RuntimeHostTuiContext {
@@ -92,7 +92,7 @@ export interface RuntimeHostTuiContext {
   };
   readonly recap: SessionRecapGenerator;
   readonly onboarding: ReturnType<typeof createRuntimeHostOnboardingSurface>;
-  readonly mcp?: TuiMcpSurface;
+  readonly mcp?: TuiMcpManagement;
   readonly profile: RuntimeHostProfile;
   close(): Promise<void>;
 }
diff --git a/packages/cli/src/tui-mcp-control.ts b/packages/cli/src/tui-mcp-control.ts
index ff91bd6c6..dfaab6406 100644
--- a/packages/cli/src/tui-mcp-control.ts
+++ b/packages/cli/src/tui-mcp-control.ts
@@ -17,10 +17,28 @@
  * under the License.
  */
 
-import type { McpServerStatus } from '@maka/core/mcp';
+import { createHash, randomUUID } from 'node:crypto';
+import {
+  MCP_CONFIG_VERSION,
+  mcpConfigChangeRetiresCredentials,
+  resolveMcpProtocolPreference,
+  type McpConfigFile,
+  type McpConfigSourceFailureReason,
+  type McpProtocolPreference,
+  type McpServerConfig,
+  type McpServerStatus,
+  type McpTestResult,
+} from '@maka/core/mcp';
 import { createCredentialMcpOAuthStorage, McpClientManager } from '@maka/mcp';
 import { createFileCredentialStore } from '@maka/storage/credential-store';
-import { createMcpConfigStore, type McpConfigStore } from '@maka/storage/mcp-config-store';
+import {
+  createMcpConfigStore,
+  assertMcpEndpointPolicyOnChanges,
+  McpConfigSourceError,
+  normalizeMcpConfig,
+  normalizeMcpImport,
+  type McpConfigStore,
+} from '@maka/storage/mcp-config-store';
 import type {
   ClientCapabilityProvider,
   RuntimeHostConnectionAvailability,
@@ -40,7 +58,12 @@ export type TuiMcpPublicationState =
 
 export interface TuiMcpServerSnapshot {
   readonly serverId: string;
-  readonly state: McpServerStatus['state'];
+  readonly configured: boolean;
+  readonly synchronized: boolean;
+  readonly enabled?: boolean;
+  readonly configuredTransport?: 'stdio' | 'remote';
+  readonly configuredProtocol?: McpProtocolPreference;
+  readonly state?: McpServerStatus['state'];
   readonly transport?: McpServerStatus['transport'];
   readonly negotiatedProtocol?: McpServerStatus['negotiatedProtocol'];
   readonly toolCount: number;
@@ -49,6 +72,7 @@ export interface TuiMcpServerSnapshot {
 
 export interface TuiMcpSnapshot {
   readonly initialization: 'loading' | 'ready' | 'error';
+  readonly configuration: 'ready' | 'synchronizing' | 'out_of_sync';
   readonly publication: TuiMcpPublicationState;
   readonly toolCount: number;
   readonly servers: readonly TuiMcpServerSnapshot[];
@@ -59,13 +83,89 @@ export interface TuiMcpSurface {
   subscribe(listener: () => void): () => void;
 }
 
-export interface TuiMcpController extends TuiMcpSurface {
+export interface TuiMcpEditConfig {
+  readonly config: McpServerConfig;
+  readonly revision: string;
+}
+
+export interface TuiMcpImportEntry {
+  readonly serverId: string;
+  readonly change: 'add' | 'replace';
+  readonly transport: 'stdio' | 'remote';
+  readonly protocol: McpProtocolPreference;
+}
+
+export interface TuiMcpImportPreview {
+  readonly previewId: string;
+  readonly entries: readonly TuiMcpImportEntry[];
+}
+
+export type TuiMcpImportPreviewResult =
+  | { readonly status: 'ready'; readonly preview: TuiMcpImportPreview }
+  | {
+      readonly status: 'invalid';
+      readonly reason: McpConfigSourceFailureReason | 'invalid-config' | 'not-ready';
+    };
+
+export type TuiMcpAction =
+  | { readonly kind: 'add'; readonly serverId: string; readonly config: McpServerConfig }
+  | {
+      readonly kind: 'edit';
+      readonly serverId: string;
+      readonly config: McpServerConfig;
+      readonly expectedRevision: string;
+    }
+  | { readonly kind: 'commit_import'; readonly previewId: string }
+  | { readonly kind: 'set_enabled'; readonly serverId: string; readonly enabled: boolean }
+  | { readonly kind: 'remove'; readonly serverId: string }
+  | { readonly kind: 'test'; readonly serverId: string }
+  | { readonly kind: 'reconnect'; readonly serverId: string };
+
+export type TuiMcpActionEffect =
+  | 'published'
+  | 'pending_host'
+  | 'sync_failed'
+  | 'publication_failed';
+
+export type TuiMcpActionResult =
+  | { readonly status: 'applied'; readonly effect: TuiMcpActionEffect }
+  | { readonly status: 'tested'; readonly test: McpTestResult; readonly effect: TuiMcpActionEffect }
+  | {
+      readonly status: 'conflict';
+      readonly reason: 'exists' | 'stale_config' | 'stale_edit' | 'stale_import' | 'missing';
+    }
+  | {
+      readonly status: 'failed';
+      readonly reason:
+        | 'closed'
+        | 'invalid-config'
+        | 'credential-cleanup-failed'
+        | 'persist-failed'
+        | 'manager-failed';
+    };
+
+export interface TuiMcpManagement extends TuiMcpSurface {
+  configForEdit(serverId: string): TuiMcpEditConfig | undefined;
+  previewImport(source: string): TuiMcpImportPreviewResult;
+  discardImportPreview(previewId: string): void;
+  execute(action: TuiMcpAction): Promise<TuiMcpActionResult>;
+}
+
+export interface TuiMcpController extends TuiMcpManagement {
   close(): Promise<void>;
 }
 
 type TuiMcpManager = Pick<
   McpClientManager,
-  'sync' | 'statuses' | 'toolSnapshot' | 'callTool' | 'onChange' | 'close'
+  | 'sync'
+  | 'statuses'
+  | 'toolSnapshot'
+  | 'callTool'
+  | 'onChange'
+  | 'test'
+  | 'reconnect'
+  | 'forgetServerCredentials'
+  | 'close'
 >;
 
 type TuiMcpConnection = Pick<
@@ -74,7 +174,7 @@ type TuiMcpConnection = Pick<
 >;
 
 interface TuiMcpControllerDeps {
-  readonly configStore: Pick<McpConfigStore, 'get'>;
+  readonly configStore: Pick<McpConfigStore, 'get' | 'transform'>;
   readonly manager: TuiMcpManager;
   readonly createProvider: (manager: TuiMcpManager) => ClientCapabilityProvider | undefined;
 }
@@ -109,6 +209,16 @@ class TuiMcpControllerImpl implements TuiMcpController {
   readonly #initialization: Promise<void>;
   #availability: RuntimeHostConnectionAvailability = { kind: 'unavailable' };
   #closed = false;
+  #config: McpConfigFile | undefined;
+  #preparedImport:
+    | {
+        readonly previewId: string;
+        readonly imported: McpConfigFile;
+        readonly basis: ReadonlyMap<string, string>;
+      }
+    | undefined;
+  #actionLane: Promise<void> = Promise.resolve();
+  #publicationSuppressed = false;
   #publicationRequested = false;
   #publicationTask: Promise<void> | undefined;
   #published:
@@ -120,6 +230,7 @@ class TuiMcpControllerImpl implements TuiMcpController {
     | undefined;
   #snapshot: TuiMcpSnapshot = freezeSnapshot({
     initialization: 'loading',
+    configuration: 'synchronizing',
     publication: 'waiting',
     toolCount: 0,
     servers: [],
@@ -131,7 +242,9 @@ class TuiMcpControllerImpl implements TuiMcpController {
     this.#disposeManagerChange = deps.manager.onChange(() => {
       try {
         this.#refreshManagerSnapshot();
-        if (this.#snapshot.initialization === 'ready') this.#requestPublication();
+        if (this.#snapshot.initialization === 'ready' && !this.#publicationSuppressed) {
+          this.#requestPublication();
+        }
       } catch {
         // An observation must never break the MCP manager's state transition.
       }
@@ -160,18 +273,72 @@ class TuiMcpControllerImpl implements TuiMcpController {
     return () => this.#listeners.delete(listener);
   }
 
+  configForEdit(serverId: string): TuiMcpEditConfig | undefined {
+    const config = this.#config?.mcpServers[serverId];
+    if (!config) return undefined;
+    return { config: structuredClone(config), revision: configRevision(config) };
+  }
+
+  previewImport(source: string): TuiMcpImportPreviewResult {
+    const current = this.#config;
+    if (this.#closed || !current || this.#snapshot.initialization !== 'ready') {
+      return { status: 'invalid', reason: 'not-ready' };
+    }
+    let imported: McpConfigFile;
+    try {
+      imported = normalizeMcpImport(source);
+    } catch (error) {
+      this.#preparedImport = undefined;
+      return {
+        status: 'invalid',
+        reason: error instanceof McpConfigSourceError ? error.reason : 'invalid-config',
+      };
+    }
+    const previewId = randomUUID();
+    const basis = new Map<string, string>();
+    const entries = Object.entries(imported.mcpServers).map(([serverId, config]) => {
+      const previous = current.mcpServers[serverId];
+      basis.set(serverId, configRevision(previous));
+      return Object.freeze({
+        serverId,
+        change: previous ? ('replace' as const) : ('add' as const),
+        transport: 'command' in config ? ('stdio' as const) : ('remote' as const),
+        protocol: resolveMcpProtocolPreference(config),
+      });
+    });
+    this.#preparedImport = { previewId, imported, basis };
+    return {
+      status: 'ready',
+      preview: Object.freeze({ previewId, entries: Object.freeze(entries) }),
+    };
+  }
+
+  discardImportPreview(previewId: string): void {
+    if (this.#preparedImport?.previewId === previewId) this.#preparedImport = undefined;
+  }
+
+  execute(action: TuiMcpAction): Promise<TuiMcpActionResult> {
+    if (this.#closed) return Promise.resolve({ status: 'failed', reason: 'closed' });
+    return this.#serializeAction(() => this.#executeAction(action));
+  }
+
   async close(): Promise<void> {
     if (this.#closed) return;
     this.#closed = true;
     this.#disposeManagerChange();
     this.#disposeConnectionAvailability();
     this.#listeners.clear();
+    this.#preparedImport = undefined;
+    this.#publicationRequested = false;
+    const managerClosing = this.#deps.manager.close();
+    await this.#actionLane.catch(() => undefined);
+    this.#config = undefined;
     await this.#publicationTask?.catch(() => undefined);
     if (this.#availability.kind === 'connected') {
       await this.#connection.unregisterClientCapabilities().catch(() => undefined);
     }
     this.#published = undefined;
-    await this.#deps.manager.close();
+    await managerClosing;
     await this.#initialization.catch(() => undefined);
   }
 
@@ -181,7 +348,8 @@ class TuiMcpControllerImpl implements TuiMcpController {
       if (this.#closed) return;
       await this.#deps.manager.sync(config);
       if (this.#closed) return;
-      this.#refreshManagerSnapshot('ready');
+      this.#config = cloneConfig(config);
+      this.#refreshManagerSnapshot('ready', 'ready');
       this.#requestPublication();
     } catch {
       if (this.#closed) return;
@@ -189,19 +357,191 @@ class TuiMcpControllerImpl implements TuiMcpController {
     }
   }
 
-  #refreshManagerSnapshot(initialization = this.#snapshot.initialization): void {
+  #serializeAction(work: () => Promise<TuiMcpActionResult>): Promise<TuiMcpActionResult> {
+    const run = this.#actionLane.then(work, work);
+    this.#actionLane = run.then(
+      () => undefined,
+      () => undefined,
+    );
+    return run;
+  }
+
+  async #executeAction(action: TuiMcpAction): Promise<TuiMcpActionResult> {
+    if (this.#closed) return { status: 'failed', reason: 'closed' };
+    if (action.kind === 'test') {
+      try {
+        const test = await this.#deps.manager.test(action.serverId);
+        return { status: 'tested', test, effect: await this.#settlePublication() };
+      } catch {
+        return { status: 'failed', reason: 'manager-failed' };
+      }
+    }
+    if (action.kind === 'reconnect') {
+      try {
+        await this.#deps.manager.reconnect(action.serverId);
+        return { status: 'applied', effect: await this.#settlePublication() };
+      } catch {
+        this.#refreshManagerSnapshot();
+        return { status: 'failed', reason: 'manager-failed' };
+      }
+    }
+    const result = await this.#commitMutation(action);
+    if (action.kind === 'commit_import') this.discardImportPreview(action.previewId);
+    return result;
+  }
+
+  async #commitMutation(
+    action: Exclude<TuiMcpAction, { kind: 'test' | 'reconnect' }>,
+  ): Promise<TuiMcpActionResult> {
+    let committed: McpConfigFile;
+    try {
+      committed = await this.#deps.configStore.transform(async (current) => {
+        if (this.#closed) {
+          throw new TuiMcpMutationError({ status: 'failed', reason: 'closed' });
+        }
+        const prepared = this.#prepareMutation(current, action);
+        if ('status' in prepared) throw new TuiMcpMutationError(prepared);
+        const { next } = prepared;
+        try {
+          assertMcpEndpointPolicyOnChanges(current, next);
+        } catch {
+          throw new TuiMcpMutationError({ status: 'failed', reason: 'invalid-config' });
+        }
+        try {
+          for (const [serverId, previous] of Object.entries(current.mcpServers)) {
+            if (!mcpConfigChangeRetiresCredentials(previous, next.mcpServers[serverId])) continue;
+            await this.#deps.manager.forgetServerCredentials(serverId, previous);
+            if (this.#closed) {
+              throw new TuiMcpMutationError({ status: 'failed', reason: 'closed' });
+            }
+          }
+        } catch (error) {
+          if (error instanceof TuiMcpMutationError) throw error;
+          throw new TuiMcpMutationError({
+            status: 'failed',
+            reason: 'credential-cleanup-failed',
+          });
+        }
+        return next;
+      });
+    } catch (error) {
+      if (error instanceof TuiMcpMutationError) return error.result;
+      return { status: 'failed', reason: 'persist-failed' };
+    }
+    if (this.#closed) return { status: 'failed', reason: 'closed' };
+    this.#preparedImport = undefined;
+    this.#config = cloneConfig(committed);
+    this.#updateSnapshot({ configuration: 'synchronizing' });
+    this.#refreshManagerSnapshot();
+    this.#publicationSuppressed = true;
+    try {
+      await this.#deps.manager.sync(committed);
+    } catch {
+      this.#publicationSuppressed = false;
+      this.#updateSnapshot({ configuration: 'out_of_sync' });
+      this.#refreshManagerSnapshot();
+      await this.#settlePublication();
+      return { status: 'applied', effect: 'sync_failed' };
+    }
+    this.#publicationSuppressed = false;
+    if (this.#closed) return { status: 'failed', reason: 'closed' };
+    this.#updateSnapshot({ configuration: 'ready' });
+    this.#refreshManagerSnapshot();
+    return { status: 'applied', effect: await this.#settlePublication() };
+  }
+
+  #prepareMutation(
+    current: McpConfigFile,
+    action: Exclude<TuiMcpAction, { kind: 'test' | 'reconnect' }>,
+  ):
+    | { readonly next: McpConfigFile }
+    | Extract<TuiMcpActionResult, { status: 'conflict' | 'failed' }> {
+    const servers = { ...current.mcpServers };
+    if (action.kind === 'add') {
+      if (Object.hasOwn(servers, action.serverId)) return { status: 'conflict', reason: 'exists' };
+      servers[action.serverId] = action.config;
+    } else if (action.kind === 'edit') {
+      const previous = servers[action.serverId];
+      if (!previous) return { status: 'conflict', reason: 'missing' };
+      if (configRevision(previous) !== action.expectedRevision) {
+        return { status: 'conflict', reason: 'stale_edit' };
+      }
+      servers[action.serverId] = action.config;
+    } else if (action.kind === 'set_enabled') {
+      const previous = servers[action.serverId];
+      if (!previous) return { status: 'conflict', reason: 'missing' };
+      servers[action.serverId] = { ...previous, enabled: action.enabled };
+    } else if (action.kind === 'remove') {
+      if (!Object.hasOwn(servers, action.serverId)) {
+        return { status: 'conflict', reason: 'missing' };
+      }
+      delete servers[action.serverId];
+    } else {
+      const prepared = this.#preparedImport;
+      if (!prepared || prepared.previewId !== action.previewId) {
+        return { status: 'conflict', reason: 'stale_import' };
+      }
+      for (const [serverId, revision] of prepared.basis) {
+        if (configRevision(servers[serverId]) !== revision) {
+          return { status: 'conflict', reason: 'stale_import' };
+        }
+      }
+      Object.assign(servers, prepared.imported.mcpServers);
+    }
+    try {
+      return {
+        next: normalizeMcpConfig({ version: MCP_CONFIG_VERSION, mcpServers: servers }),
+      };
+    } catch {
+      return { status: 'failed', reason: 'invalid-config' };
+    }
+  }
+
+  async #settlePublication(): Promise<TuiMcpActionEffect> {
+    this.#requestPublication();
+    while (!this.#closed && (this.#publicationTask || this.#publicationRequested)) {
+      await this.#publicationTask?.catch(() => undefined);
+    }
+    if (this.#snapshot.publication === 'error') return 'publication_failed';
+    if (this.#snapshot.publication === 'host_unavailable') return 'pending_host';
+    return 'published';
+  }
+
+  #refreshManagerSnapshot(
+    initialization = this.#snapshot.initialization,
+    configuration = this.#snapshot.configuration,
+  ): void {
     const statuses = this.#deps.manager.statuses();
+    const statusById = new Map(statuses.map((status) => [status.serverId, status]));
+    const serverIds = new Set([
+      ...Object.keys(this.#config?.mcpServers ?? {}),
+      ...statuses.map((status) => status.serverId),
+    ]);
     this.#snapshot = freezeSnapshot({
       initialization,
+      configuration,
       publication: this.#snapshot.publication,
       toolCount: this.#deps.manager.toolSnapshot().tools.length,
-      servers: statuses.map(projectServerStatus),
+      servers: [...serverIds]
+        .sort((left, right) => left.localeCompare(right))
+        .map((serverId) =>
+          projectServerStatus(
+            serverId,
+            this.#config?.mcpServers[serverId],
+            statusById.get(serverId),
+            configuration === 'ready',
+          ),
+        ),
     });
     this.#notify();
   }
 
   #requestPublication(): void {
-    if (this.#closed || this.#snapshot.initialization !== 'ready') return;
+    if (this.#closed) {
+      this.#publicationRequested = false;
+      return;
+    }
+    if (this.#snapshot.initialization !== 'ready' || this.#publicationSuppressed) return;
     this.#publicationRequested = true;
     if (this.#publicationTask) return;
     this.#publicationTask = this.#runPublicationQueue().finally(() => {
@@ -266,7 +606,9 @@ class TuiMcpControllerImpl implements TuiMcpController {
     );
   }
 
-  #updateSnapshot(update: Partial<Pick<TuiMcpSnapshot, 'initialization' | 'publication'>>): void {
+  #updateSnapshot(
+    update: Partial<Pick<TuiMcpSnapshot, 'initialization' | 'configuration' | 'publication'>>,
+  ): void {
     this.#snapshot = freezeSnapshot({ ...this.#snapshot, ...update });
     this.#notify();
   }
@@ -282,14 +624,28 @@ class TuiMcpControllerImpl implements TuiMcpController {
   }
 }
 
-function projectServerStatus(status: McpServerStatus): TuiMcpServerSnapshot {
+function projectServerStatus(
+  serverId: string,
+  config: McpServerConfig | undefined,
+  status: McpServerStatus | undefined,
+  configurationSynchronized: boolean,
+): TuiMcpServerSnapshot {
   return {
-    serverId: status.serverId,
-    state: status.state,
-    ...(status.transport ? { transport: status.transport } : {}),
-    ...(status.negotiatedProtocol ? { negotiatedProtocol: status.negotiatedProtocol } : {}),
-    toolCount: status.toolCount,
-    ...(status.error ? { error: status.error } : {}),
+    serverId,
+    configured: config !== undefined,
+    synchronized: configurationSynchronized && config !== undefined && status !== undefined,
+    ...(config
+      ? {
+          enabled: config.enabled !== false,
+          configuredTransport: 'command' in config ? ('stdio' as const) : ('remote' as const),
+          configuredProtocol: resolveMcpProtocolPreference(config),
+        }
+      : {}),
+    ...(status ? { state: status.state } : {}),
+    ...(status?.transport ? { transport: status.transport } : {}),
+    ...(status?.negotiatedProtocol ? { negotiatedProtocol: status.negotiatedProtocol } : {}),
+    toolCount: status?.toolCount ?? 0,
+    ...(status?.error ? { error: status.error } : {}),
   };
 }
 
@@ -313,3 +669,18 @@ async function closeProvider(provider: ClientCapabilityProvider | undefined): Pr
     // A rejected provider never crossed into Host ownership.
   }
 }
+
+function cloneConfig(config: McpConfigFile): McpConfigFile {
+  return structuredClone(config);
+}
+
+function configRevision(config: McpConfigFile | McpServerConfig | undefined): string {
+  if (!config) return 'missing';
+  return createHash('sha256').update(JSON.stringify(config)).digest('hex');
+}
+
+class TuiMcpMutationError extends Error {
+  constructor(readonly result: Extract<TuiMcpActionResult, { status: 'conflict' | 'failed' }>) {
+    super(result.reason);
+  }
+}
diff --git a/packages/cli/src/tui-primary-guidance.ts b/packages/cli/src/tui-primary-guidance.ts
index 6c0ddd634..552c41df0 100644
--- a/packages/cli/src/tui-primary-guidance.ts
+++ b/packages/cli/src/tui-primary-guidance.ts
@@ -56,7 +56,7 @@ const TUI_PRIMARY_GUIDANCE = {
       goal: '查看自主目标状态',
       graph: '查看、启用、停用 Graph 模式,或执行一次 Graph 任务',
       help: '查看命令和快捷键',
-      mcp: '查看客户端 MCP 服务器和工具发布状态',
+      mcp: '管理客户端 MCP 服务器和工具发布状态',
       model: '选择模型',
       move: '将当前会话移到其他目录',
       new: '新建会话',
@@ -106,7 +106,7 @@ const TUI_PRIMARY_GUIDANCE = {
       goal: 'Show autonomous goal status',
       graph: 'Show, enable, disable, or run one Graph turn',
       help: 'Show commands and keybindings',
-      mcp: 'Show client-owned MCP servers and tool publication status',
+      mcp: 'Manage client-owned MCP servers and tool publication status',
       model: 'Select model',
       move: 'Move current session to another directory',
       new: 'Start a new session',
diff --git a/packages/core/src/__tests__/mcp-credential-retirement.test.ts b/packages/core/src/__tests__/mcp-credential-retirement.test.ts
new file mode 100644
index 000000000..3d3afb29b
--- /dev/null
+++ b/packages/core/src/__tests__/mcp-credential-retirement.test.ts
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import assert from 'node:assert/strict';
+import { describe, it } from 'node:test';
+import { mcpConfigChangeRetiresCredentials, type McpServerConfig } from '../mcp.js';
+
+describe('MCP credential retirement', () => {
+  const stdio: McpServerConfig = { command: 'server' };
+  const remote: McpServerConfig = { url: 'https://one.example/mcp' };
+
+  it('retires credentials when an id is removed, repointed, or changes transport', () => {
+    assert.equal(mcpConfigChangeRetiresCredentials(remote, undefined), true);
+    assert.equal(
+      mcpConfigChangeRetiresCredentials(remote, { url: 'https://two.example/mcp' }),
+      true,
+    );
+    assert.equal(mcpConfigChangeRetiresCredentials(remote, stdio), true);
+    // A stale credential record can exist under a currently-stdio id after
+    // an offline edit. Do not let it become the new remote endpoint's token.
+    assert.equal(mcpConfigChangeRetiresCredentials(stdio, remote), true);
+  });
+
+  it('keeps credentials when endpoint ownership does not change', () => {
+    assert.equal(
+      mcpConfigChangeRetiresCredentials(remote, {
+        ...remote,
+        headers: { Authorization: 'Bearer replacement' },
+        enabled: false,
+      }),
+      false,
+    );
+    assert.equal(mcpConfigChangeRetiresCredentials(stdio, { command: 'different-server' }), false);
+  });
+});
diff --git a/packages/core/src/mcp.ts b/packages/core/src/mcp.ts
index f057f5c15..2cc941ed7 100644
--- a/packages/core/src/mcp.ts
+++ b/packages/core/src/mcp.ts
@@ -343,6 +343,20 @@ export function isMcpStdioConfig(config: McpServerConfig): config is McpStdioSer
   return 'command' in config;
 }
 
+/** True when a configuration change can leave credentials bound to an
+ * endpoint the server id no longer names. Removals retire regardless of
+ * transport so a stale record cannot survive the id being freed for reuse. */
+export function mcpConfigChangeRetiresCredentials(
+  previous: McpServerConfig,
+  next: McpServerConfig | undefined,
+): boolean {
+  if (!next) return true;
+  const previousStdio = isMcpStdioConfig(previous);
+  const nextStdio = isMcpStdioConfig(next);
+  if (previousStdio || nextStdio) return previousStdio !== nextStdio;
+  return previous.url !== next.url;
+}
+
 export function resolveMcpProtocolPreference(config: McpServerConfig): McpProtocolPreference {
   return config.protocol ?? 'legacy';
 }
diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts
index a7010d469..38d809ff1 100644
--- a/packages/mcp/src/index.ts
+++ b/packages/mcp/src/index.ts
@@ -46,6 +46,7 @@ import {
 import {
   isMcpStdioConfig,
   isNonLoopbackCleartextHttp,
+  mcpConfigChangeRetiresCredentials,
   resolveMcpProtocolPreference,
   type McpBoundTool,
   type McpCallResult,
@@ -444,7 +445,7 @@ export class McpClientManager {
         // retries the erase against the still-owed old config.
         const owed = current.credentialCleanupOwed
           ? current.credentialCleanupOwed
-          : remoteUrlChanged(current.config, serverConfig)
+          : mcpConfigChangeRetiresCredentials(current.config, serverConfig)
             ? current.config
             : undefined;
         if (owed) {
@@ -1596,8 +1597,11 @@ export class McpClientManager {
    * its config is removed, so a delete failure aborts the removal while
    * everything is still recoverable — instead of leaving an orphaned token
    * a same-id re-add would inherit. */
-  async forgetServerCredentials(serverId: string): Promise<void> {
-    await this.forgetAuthorization(serverId, this.connections.get(serverId)?.config);
+  async forgetServerCredentials(
+    serverId: string,
+    previousConfig = this.connections.get(serverId)?.config,
+  ): Promise<void> {
+    await this.forgetAuthorization(serverId, previousConfig);
   }
 
   /** Drops any stored OAuth record for a server that is being removed or
@@ -2571,16 +2575,6 @@ async function connectCandidate(
   }
 }
 
-/** True when a reconfigured server no longer talks to the endpoint its
- * stored credentials were issued for: the URL changed, or the entry
- * switched between stdio and remote. A headers-only edit returns false. */
-function remoteUrlChanged(previous: McpServerConfig, next: McpServerConfig): boolean {
-  const previousStdio = isMcpStdioConfig(previous);
-  const nextStdio = isMcpStdioConfig(next);
-  if (previousStdio || nextStdio) return previousStdio !== nextStdio;
-  return previous.url !== next.url;
-}
-
 function stableConfigFingerprint(config: McpServerConfig): string {
   return JSON.stringify(sortValue(config));
 }
diff --git a/packages/storage/src/__tests__/fixtures/mcp-config-lock-holder.ts b/packages/storage/src/__tests__/fixtures/mcp-config-lock-holder.ts
new file mode 100644
index 000000000..3c34f50bd
--- /dev/null
+++ b/packages/storage/src/__tests__/fixtures/mcp-config-lock-holder.ts
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { createMcpConfigStore } from '../../mcp-config-store.js';
+
+const root = process.argv[2];
+if (!root) throw new Error('Missing MCP config workspace root');
+
+await createMcpConfigStore(root).transform(async (current) => {
+  process.send?.('locked');
+  await new Promise<never>(() => setInterval(() => undefined, 1_000));
+  return current;
+});
diff --git a/packages/storage/src/__tests__/mcp-config-store.test.ts b/packages/storage/src/__tests__/mcp-config-store.test.ts
index 853cf634f..faff8d4da 100644
--- a/packages/storage/src/__tests__/mcp-config-store.test.ts
+++ b/packages/storage/src/__tests__/mcp-config-store.test.ts
@@ -18,6 +18,7 @@
  */
 
 import assert from 'node:assert/strict';
+import { fork } from 'node:child_process';
 import { mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
 import { tmpdir } from 'node:os';
 import { join } from 'node:path';
@@ -218,7 +219,8 @@ test('allows SSE only with an omitted or explicit legacy protocol', () => {
 test('transform sees the latest committed config, not a caller snapshot', async () => {
   // The restore-plus-mutation seam: a marker-bearing write that derived its
   // restores from a stale snapshot could roll a rotated secret back. Inside
-  // transform, apply() must observe the concurrent writer's commit.
+  // transform, apply() must observe the concurrent writer's commit under the
+  // shared file transaction.
   const root = await tempRoot();
   const store = createMcpConfigStore(root);
   await store.upsert('local', { command: 'npx', env: { TOKEN: 'v1' } });
@@ -236,6 +238,65 @@ test('transform sees the latest committed config, not a caller snapshot', async
   assert.equal(final.env?.TOKEN, 'v2-rotated');
 });
 
+test('two independent stores preserve concurrent additions to one workspace', async () => {
+  const root = await tempRoot();
+  await createMcpConfigStore(root).get();
+  const desktop = createMcpConfigStore(root);
+  const tui = createMcpConfigStore(root);
+
+  await Promise.all([
+    desktop.upsert('desktop', { command: 'desktop-server' }),
+    tui.upsert('tui', { command: 'tui-server' }),
+  ]);
+
+  const saved = await createMcpConfigStore(root).get();
+  assert.equal(
+    saved.mcpServers.desktop && 'command' in saved.mcpServers.desktop
+      ? saved.mcpServers.desktop.command
+      : undefined,
+    'desktop-server',
+  );
+  assert.equal(
+    saved.mcpServers.tui && 'command' in saved.mcpServers.tui
+      ? saved.mcpServers.tui.command
+      : undefined,
+    'tui-server',
+  );
+});
+
+test('a new store commits after a killed MCP config writer releases its native lock', async (t) => {
+  const root = await tempRoot();
+  const holder = fork(new URL('./fixtures/mcp-config-lock-holder.js', import.meta.url), [root], {
+    stdio: ['ignore', 'ignore', 'inherit', 'ipc'],
+  });
+  t.after(() => {
+    if (holder.exitCode === null && holder.signalCode === null) holder.kill('SIGKILL');
+  });
+  await new Promise<void>((resolve, reject) => {
+    holder.once('message', (message) => {
+      if (message === 'locked') resolve();
+      else reject(new Error(`Unexpected child message: ${String(message)}`));
+    });
+    holder.once('error', reject);
+    holder.once('exit', (code, signal) => {
+      reject(new Error(`MCP config lock holder exited early (${String(code)}, ${signal})`));
+    });
+  });
+
+  holder.kill('SIGKILL');
+  await new Promise<void>((resolve) => holder.once('exit', () => resolve()));
+
+  const saved = await createMcpConfigStore(root).upsert('recovered', {
+    command: 'recovered-server',
+  });
+  const recovered = saved.mcpServers.recovered;
+  assert.ok(recovered && 'command' in recovered);
+  assert.equal(recovered.command, 'recovered-server');
+  const reopened = (await createMcpConfigStore(root).get()).mcpServers.recovered;
+  assert.ok(reopened && 'command' in reopened);
+  assert.equal(reopened.command, 'recovered-server');
+});
+
 test('serializes concurrent updates without corrupting the file', async () => {
   const root = await tempRoot();
   const store = createMcpConfigStore(root);
diff --git a/packages/storage/src/mcp-config-store.ts b/packages/storage/src/mcp-config-store.ts
index 4003c8c4b..c362d86ed 100644
--- a/packages/storage/src/mcp-config-store.ts
+++ b/packages/storage/src/mcp-config-store.ts
@@ -32,6 +32,7 @@ import {
   type McpServerConfig,
   type McpStdioServerConfig,
 } from '@maka/core/mcp';
+import { withProcessLifetimeFileUpdateLock } from './process-lifetime-file-update-lock.js';
 
 const MAX_SERVERS = 100;
 const MAX_ID_LENGTH = 128;
@@ -41,11 +42,13 @@ const FORBIDDEN_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
 
 export interface McpConfigStore {
   get(): Promise<McpConfigFile>;
-  /** One serialized read-transform-write. `apply` sees the CURRENT on-disk
-   * config and returns the next one, inside the store's write queue — the
-   * seam for restore-plus-mutation flows whose separate get()-then-write
-   * would race a concurrent writer and roll a rotated secret back. */
-  transform(apply: (current: McpConfigFile) => McpConfigFile): Promise<McpConfigFile>;
+  /** One cross-process read-transform-write transaction. `apply` sees the
+   * current on-disk config and may finish asynchronous effects that must
+   * precede the commit, such as retiring credentials. The shared file lock
+   * remains held until the replacement document is durable. */
+  transform(
+    apply: (current: McpConfigFile) => McpConfigFile | Promise<McpConfigFile>,
+  ): Promise<McpConfigFile>;
   upsert(serverId: string, config: McpServerConfig): Promise<McpConfigFile>;
   remove(serverId: string): Promise<McpConfigFile>;
 }
@@ -124,19 +127,26 @@ export function normalizeMcpImport(source: string): McpConfigFile {
 }
 
 class FileMcpConfigStore implements McpConfigStore {
-  private queue: Promise<void> = Promise.resolve();
+  private directoryReady: Promise<void> | undefined;
 
   constructor(private readonly path: string) {}
 
   async get(): Promise<McpConfigFile> {
-    return this.serial(async () => this.readOrCreate());
+    try {
+      return await this.read();
+    } catch (error) {
+      if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
+      return this.withUpdateLock(() => this.readOrCreate());
+    }
   }
 
-  async transform(apply: (current: McpConfigFile) => McpConfigFile): Promise<McpConfigFile> {
-    return this.serial(async () => {
+  async transform(
+    apply: (current: McpConfigFile) => McpConfigFile | Promise<McpConfigFile>,
+  ): Promise<McpConfigFile> {
+    return this.withUpdateLock(async () => {
       const current = await this.readOrCreate();
-      const next = normalizeMcpConfig(apply(current));
-      enforceEndpointPolicyOnChanges(current, next);
+      const next = normalizeMcpConfig(await apply(current));
+      assertMcpEndpointPolicyOnChanges(current, next);
       await this.write(next);
       return next;
     });
@@ -144,35 +154,33 @@ class FileMcpConfigStore implements McpConfigStore {
 
   async upsert(serverId: string, config: McpServerConfig): Promise<McpConfigFile> {
     assertSafeKey(serverId, 'server id');
-    return this.serial(async () => {
-      const current = await this.readOrCreate();
-      const next = normalizeMcpConfig({
+    return this.transform((current) =>
+      normalizeMcpConfig({
         version: MCP_CONFIG_VERSION,
         mcpServers: { ...current.mcpServers, [serverId]: config },
-      });
-      enforceEndpointPolicyOnChanges(current, next);
-      await this.write(next);
-      return next;
-    });
+      }),
+    );
   }
 
   async remove(serverId: string): Promise<McpConfigFile> {
     assertSafeKey(serverId, 'server id');
-    return this.serial(async () => {
-      const current = await this.readOrCreate();
+    return this.transform((current) => {
       const { [serverId]: _removed, ...mcpServers } = current.mcpServers;
-      const next: McpConfigFile = { version: MCP_CONFIG_VERSION, mcpServers };
-      await this.write(next);
-      return next;
+      return { version: MCP_CONFIG_VERSION, mcpServers };
     });
   }
 
+  private async read(): Promise<McpConfigFile> {
+    const text = await readFile(this.path, 'utf8');
+    if (Buffer.byteLength(text, 'utf8') > MAX_CONFIG_BYTES) {
+      throw new Error('MCP config exceeds 1 MiB');
+    }
+    return normalizeMcpConfig(JSON.parse(text));
+  }
+
   private async readOrCreate(): Promise<McpConfigFile> {
     try {
-      const text = await readFile(this.path, 'utf8');
-      if (Buffer.byteLength(text, 'utf8') > MAX_CONFIG_BYTES)
-        throw new Error('MCP config exceeds 1 MiB');
-      return normalizeMcpConfig(JSON.parse(text));
+      return await this.read();
     } catch (error) {
       if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
       const empty = createDefaultMcpConfig();
@@ -181,10 +189,14 @@ class FileMcpConfigStore implements McpConfigStore {
     }
   }
 
+  private async withUpdateLock<T>(operation: () => Promise<T>): Promise<T> {
+    await this.ensureDirectory();
+    return withProcessLifetimeFileUpdateLock(this.path, operation);
+  }
+
   private async write(config: McpConfigFile): Promise<void> {
     const dir = dirname(this.path);
-    await mkdir(dir, { recursive: true, mode: 0o700 });
-    if (process.platform !== 'win32') await chmod(dir, 0o700);
+    await this.ensureDirectory();
     const tempPath = join(dir, `.mcp-${randomUUID()}.tmp`);
     try {
       await writeFile(tempPath, `${JSON.stringify(config, null, 2)}\n`, {
@@ -200,18 +212,18 @@ class FileMcpConfigStore implements McpConfigStore {
     }
   }
 
-  private async serial<T>(operation: () => Promise<T>): Promise<T> {
-    const previous = this.queue;
-    let release!: () => void;
-    this.queue = new Promise<void>((resolve) => {
-      release = resolve;
+  private ensureDirectory(): Promise<void> {
+    if (this.directoryReady) return this.directoryReady;
+    const dir = dirname(this.path);
+    const ready = (async () => {
+      await mkdir(dir, { recursive: true, mode: 0o700 });
+      if (process.platform !== 'win32') await chmod(dir, 0o700);
+    })();
+    this.directoryReady = ready;
+    void ready.catch(() => {
+      if (this.directoryReady === ready) this.directoryReady = undefined;
     });
-    await previous;
-    try {
-      return await operation();
-    } finally {
-      release();
-    }
+    return ready;
   }
 }
 
@@ -234,7 +246,7 @@ export function assertMcpEndpointPolicy(server: McpServerConfig, serverId: strin
   }
 }
 
-function enforceEndpointPolicyOnChanges(
+export function assertMcpEndpointPolicyOnChanges(
   previous: McpConfigFile | undefined,
   next: McpConfigFile,
 ): void {


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org