Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/desktop/src/main/__tests__/mcp-ipc-main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,15 @@ test('a URL change retires the old endpoint credentials before the write, and an
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, []);

// Same repoint with a healthy credential store: erase strictly precedes
// the write. An unchanged-URL upsert afterwards does not erase at all.
eraseFails = false;
Expand Down
11 changes: 4 additions & 7 deletions apps/desktop/src/main/mcp-ipc-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import type { IpcMain } from 'electron';
import {
MCP_CONFIG_VERSION,
isMcpStdioConfig,
mcpConfigChangeRetiresCredentials,
type McpConfigAddResult,
type McpConfigFile,
type McpConfigImportResult,
Expand All @@ -29,6 +29,7 @@ import {
} from '@maka/core/mcp';
import type { McpClientManager } from '@maka/mcp';
import {
assertMcpEndpointPolicyOnChanges,
McpServerExistsError,
McpConfigSourceError,
normalizeMcpConfig,
Expand Down Expand Up @@ -110,6 +111,7 @@ export function registerMcpIpcMain(deps: McpIpcMainDeps): void {
): Promise<McpConfigFile> => {
const current = await deps.store.get();
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
Expand Down Expand Up @@ -369,12 +371,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;
}
Expand Down
34 changes: 25 additions & 9 deletions packages/cli/src/__tests__/pi-tui-mcp-status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { 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' },
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -92,6 +103,7 @@ describe('MCP status overlay', () => {
let closed = 0;
const mcp = surface({
initialization: 'ready',
configuration: 'ready',
publication: 'not_published',
toolCount: 0,
servers: [],
Expand All @@ -102,7 +114,7 @@ describe('MCP status overlay', () => {
disposed += 1;
};
};
const overlay = new McpStatusOverlay({
const overlay = new McpManagementOverlay({
locale: 'en',
surface: mcp,
viewportRows: () => 6,
Expand All @@ -121,9 +133,13 @@ describe('MCP status overlay', () => {

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' }),
};
}
187 changes: 187 additions & 0 deletions packages/cli/src/__tests__/pi-tui-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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' },
Expand All @@ -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' }),
},
});

Expand All @@ -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();
Expand Down
Loading