Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
74 changes: 0 additions & 74 deletions apps/desktop/src/main/__tests__/mcp-import.test.ts

This file was deleted.

73 changes: 30 additions & 43 deletions apps/desktop/src/main/__tests__/mcp-ipc-main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ test('MCP IPC commits config before publishing capabilities and emitting status'
config = apply(config);
return config;
},
set: async (next) => { calls.push('store'); config = next; return next; },
upsert: async (serverId, server) => {
calls.push('store');
config = { version: MCP_CONFIG_VERSION, mcpServers: { ...config.mcpServers, [serverId]: server } };
Expand Down Expand Up @@ -82,18 +81,28 @@ test('MCP IPC commits config before publishing capabilities and emitting status'
assert.deepEqual(calls, ['store', 'sync', 'emit', 'publish']);

calls.length = 0;
const setConfig = handlers.get('mcp:setConfig');
assert.ok(setConfig);
const imported = await setConfig({}, {
version: MCP_CONFIG_VERSION,
mcpServers: { remote: { url: 'https://example.com/mcp', enabled: false } },
});
assert.deepEqual(imported.mcpServers, {
remote: { url: 'https://example.com/mcp', enabled: false },
const importConfig = handlers.get('mcp:importConfig');
assert.ok(importConfig);
const imported = await importConfig(
{},
'{"remote":{"url":"https://example.com/mcp","enabled":false}}',
);
assert.equal(imported.status, 'imported');
assert.deepEqual(imported.config.mcpServers, {
fixture: { command: 'node' },
remote: { url: 'https://example.com/mcp', enabled: false, transport: 'auto' },
});
// The bulk edit removed `fixture`: its credentials retire BEFORE the
// config write, so a restart in between cannot orphan them.
assert.deepEqual(calls, ['forget', 'store', 'sync', 'emit', 'publish']);
assert.deepEqual(calls, ['store', 'sync', 'emit', 'publish']);

calls.length = 0;
assert.deepEqual(
await importConfig(
{},
'{"version":2,"mcpServers":{"local":{"command":"node","protocol":"auto"}}}',
),
{ status: 'invalid', reason: 'protocol-version' },
);
assert.deepEqual(calls, []);

calls.length = 0;
const add = handlers.get('mcp:add');
Expand Down Expand Up @@ -136,7 +145,6 @@ test('MCP remove aborts before touching the config when credential deletion fail
config = apply(config);
return config;
},
set: async (next) => { config = next; return next; },
upsert: async (serverId, server) => {
config = { version: MCP_CONFIG_VERSION, mcpServers: { ...config.mcpServers, [serverId]: server } };
return config;
Expand Down Expand Up @@ -204,7 +212,6 @@ test('MCP IPC redacts clientSecret toward the renderer and restores the sentinel
config = apply(config);
return config;
},
set: async (next) => { config = next; return next; },
upsert: async (serverId, server) => {
config = { version: MCP_CONFIG_VERSION, mcpServers: { ...config.mcpServers, [serverId]: server } };
return config;
Expand Down Expand Up @@ -315,7 +322,6 @@ test('MCP market cancellation waits for an in-flight config write before rolling
calls.push('write:end');
return config;
},
set: async (next) => { config = next; return next; },
upsert: async (serverId, server) => {
config = { version: MCP_CONFIG_VERSION, mcpServers: { ...config.mcpServers, [serverId]: server } };
return config;
Expand Down Expand Up @@ -389,7 +395,6 @@ test('an active login on a secret-bearing server does not veto edits to another
store: {
get: async () => config,
transform: async (apply) => { config = apply(config); return config; },
set: async (next) => { config = next; return next; },
upsert: async (_serverId, _server) => config,
remove: async () => config,
},
Expand All @@ -414,32 +419,22 @@ test('an active login on a secret-bearing server does not veto edits to another
emitChanged: () => {},
});

const getConfig = handlers.get('mcp:getConfig');
const setConfig = handlers.get('mcp:setConfig');
assert.ok(getConfig);
assert.ok(setConfig);
// The renderer edits the REDACTED config: notion comes back carrying the
// clientSecret sentinel. A sentinel-versus-real-value comparison would
// call that a change; the semantic (restored) comparison must not.
const redacted = await getConfig({});
const next = await setConfig({}, {
...redacted,
mcpServers: {
...redacted.mcpServers,
other: { command: 'node', args: ['--verbose'] },
},
});
const other = next.mcpServers.other;
const importConfig = handlers.get('mcp:importConfig');
assert.ok(importConfig);
// Import merges against the authoritative main-process config. Existing
// secrets never cross to the renderer merely to preserve an untouched entry.
const next = await importConfig({}, '{"other":{"command":"node","args":["--verbose"]}}');
assert.equal(next.status, 'imported');
const other = next.config.mcpServers.other;
assert.ok(other && 'command' in other);
assert.deepEqual(other.args, ['--verbose']);
const storedNotion = config.mcpServers.notion;
assert.ok(storedNotion && 'url' in storedNotion);
assert.equal(storedNotion.oauth?.clientSecret, 'real-secret');

// Actually touching (here: removing) the login-owned server still fails.
const { notion: _gone, ...withoutNotion } = redacted.mcpServers;
// Actually touching the login-owned server still fails.
await assert.rejects(
setConfig({}, { ...redacted, mcpServers: withoutNotion }),
importConfig({}, '{"notion":{"url":"https://other.example.com/mcp"}}'),
/login in progress/u,
);
assert.ok(config.mcpServers.notion);
Expand All @@ -458,7 +453,6 @@ test('a URL change retires the old endpoint credentials before the write, and an
store: {
get: async () => config,
transform: async (apply) => { calls.push('write'); config = apply(config); return config; },
set: async (next) => { config = next; return next; },
upsert: async (_serverId, _server) => config,
remove: async () => config,
},
Expand Down Expand Up @@ -520,7 +514,6 @@ test('cancelling an install rolls back only its own write, never a newer same-id
store: {
get: async () => config,
transform: async (apply) => { config = apply(config); return config; },
set: async (next) => { config = next; return next; },
upsert: async (_serverId, _server) => config,
remove: async () => config,
},
Expand Down Expand Up @@ -596,7 +589,6 @@ test('a login claim travels the shared lane and cannot land inside an open trans
config = apply(config);
return config;
},
set: async (next) => { config = next; return next; },
upsert: async (_serverId, _server) => config,
remove: async () => config,
},
Expand Down Expand Up @@ -725,7 +717,6 @@ test('the config write fails closed when the snapshot drifts under the transacti
wrote = true;
return next;
},
set: async (next) => next,
upsert: async (_serverId, _server) => config,
remove: async () => config,
},
Expand Down Expand Up @@ -771,10 +762,6 @@ test('MCP config commit is not rolled back by a capability publication failure',
config = apply(config);
return config;
},
set: async (next) => {
config = next;
return next;
},
upsert: async (serverId, server) => {
config = { version: MCP_CONFIG_VERSION, mcpServers: { ...config.mcpServers, [serverId]: server } };
return config;
Expand Down
49 changes: 37 additions & 12 deletions apps/desktop/src/main/__tests__/mcp-page-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ import { getMcpCopy } from '../../renderer/locales/mcp-copy.js';
import {
createEmptyMcpDraft,
mcpConfigFromDraft,
mcpDraftProtocolPreference,
mcpDraftFromConfig,
presentMcpNegotiatedProtocol,
withMcpDraftTransport,
} from '../../renderer/mcp-page-model.js';

const copy = getMcpCopy('en');
Expand Down Expand Up @@ -81,16 +81,17 @@ test('editing a remote MCP round-trips auto and exact modern preferences', () =>
}
});

test('selecting legacy SSE converges and persists the legacy preference', () => {
test('legacy SSE projects legacy without erasing an explicit remote pin', () => {
const modern = {
...createEmptyMcpDraft(),
kind: 'remote' as const,
url: 'https://example.com/sse',
protocol: '2026-07-28' as const,
};
const sse = withMcpDraftTransport(modern, 'sse');
const sse = { ...modern, transport: 'sse' as const };

assert.equal(sse.protocol, 'legacy');
assert.equal(sse.protocol, '2026-07-28');
assert.equal(mcpDraftProtocolPreference(sse), 'legacy');
assert.deepEqual(mcpConfigFromDraft(sse, copy), {
enabled: true,
url: 'https://example.com/sse',
Expand All @@ -99,20 +100,44 @@ test('selecting legacy SSE converges and persists the legacy preference', () =>
headers: {},
});

// The serializer also protects against an inconsistent draft supplied by a
// stale renderer event.
const forged = mcpConfigFromDraft({ ...sse, protocol: 'auto' }, copy);
assert.equal(!isMcpStdioConfig(forged) && forged.protocol, 'legacy');
assert.equal(
mcpDraftProtocolPreference({ ...sse, transport: 'streamable-http' }),
'2026-07-28',
);
});

test('stdio editing never adds a protocol preference in config version 2', () => {
test('a newly-authored stdio MCP persists the one-child legacy default', () => {
const saved = mcpConfigFromDraft(
{ ...createEmptyMcpDraft(), id: 'local', commandLine: ' node ' },
copy,
);

assert.equal(isMcpStdioConfig(saved), true);
assert.equal(Object.hasOwn(saved, 'protocol'), false);
assert.equal(isMcpStdioConfig(saved) && saved.protocol, 'legacy');
});

test('stdio editing presents omitted legacy and round-trips explicit modern preferences', () => {
const omitted = mcpDraftFromConfig('local', { command: 'node' });
assert.equal(mcpDraftProtocolPreference(omitted), 'legacy');
const savedOmitted = mcpConfigFromDraft(omitted, copy);
assert.ok(isMcpStdioConfig(savedOmitted));
assert.equal(savedOmitted.protocol, 'legacy');

for (const protocol of ['auto', '2026-07-28'] as const) {
const draft = mcpDraftFromConfig('local', { command: 'node', protocol });
const saved = mcpConfigFromDraft(draft, copy);
assert.equal(isMcpStdioConfig(saved) && saved.protocol, protocol);
}
});

test('kind changes preserve an explicit protocol choice and derive only unselected defaults', () => {
const empty = createEmptyMcpDraft();
assert.equal(mcpDraftProtocolPreference(empty), 'legacy');
assert.equal(mcpDraftProtocolPreference({ ...empty, kind: 'remote' }), 'auto');

const pinned = { ...empty, kind: 'remote' as const, protocol: '2026-07-28' as const };
assert.equal(mcpDraftProtocolPreference({ ...pinned, kind: 'stdio' }), '2026-07-28');
assert.equal(mcpDraftProtocolPreference(pinned), '2026-07-28');
});

test('the MCP catalog opts every bundled remote entry into auto negotiation', () => {
Expand Down Expand Up @@ -141,7 +166,7 @@ test('an edit that does not touch OAuth preserves the block through save', () =>
},
};
const draft = mcpDraftFromConfig('notion', stored);
const saved = mcpConfigFromDraft(withMcpDraftTransport(draft, 'sse'), copy);
const saved = mcpConfigFromDraft({ ...draft, transport: 'sse' }, copy);
assert.ok('url' in saved);
assert.equal(saved.transport, 'sse');
assert.deepEqual(saved.oauth, stored.oauth);
Expand All @@ -165,7 +190,7 @@ test('a stdio config round-trips through the command-line field', () => {
env: { TOKEN: 'secret' },
};
const saved = mcpConfigFromDraft(mcpDraftFromConfig('local', stored), copy);
assert.deepEqual(saved, stored);
assert.deepEqual(saved, { ...stored, protocol: 'legacy' });
});

test('status copy presents only a live connected negotiated protocol', () => {
Expand Down
Loading