mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
* feat(daemon): add run-scoped MCP tool bundles * fix(daemon): keep sandbox runs in managed project dirs * fix(daemon): reject malformed run tool bundles * fix(contracts): model run-scoped mcp server inputs * fix(daemon): reject unsupported run tool bundles * fix(daemon): validate run tools before chat fallback * test(daemon): expect sandbox imported folder failure * fix(daemon): preflight sandbox project roots before run rows * fix(daemon): preflight sandbox chat project roots * fix(daemon): allow host editor for sandbox imports * fix(daemon): preflight sandbox routine project reuse * fix(daemon): reject undeliverable Claude tool bundles * fix(daemon): single-source chat route validation
231 lines
6.2 KiB
TypeScript
231 lines
6.2 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
normalizeRunToolBundleForRun,
|
|
parseRunToolBundleForRequest,
|
|
resolveExternalMcpServersForRun,
|
|
summarizeRunToolBundle,
|
|
validateRunToolBundleForAgent,
|
|
} from '../src/run-tool-bundle.js';
|
|
|
|
describe('run-scoped tool bundles', () => {
|
|
it('sanitizes MCP servers onto the run and redacts spawn-only details in summaries', () => {
|
|
const bundle = normalizeRunToolBundleForRun({
|
|
mcpServers: [
|
|
{
|
|
id: 'local-tools',
|
|
label: 'Local tools',
|
|
transport: 'stdio',
|
|
command: 'node',
|
|
args: ['server.js', '--token=secret'],
|
|
env: { API_TOKEN: 'secret' },
|
|
},
|
|
{
|
|
id: 'remote-tools',
|
|
transport: 'http',
|
|
url: 'https://example.test/mcp',
|
|
headers: { Authorization: 'Bearer secret' },
|
|
},
|
|
{
|
|
id: '../bad',
|
|
transport: 'stdio',
|
|
command: 'node',
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(bundle.mcpServers).toHaveLength(2);
|
|
expect(bundle.mcpServers[0]).toMatchObject({
|
|
id: 'local-tools',
|
|
command: 'node',
|
|
env: { API_TOKEN: 'secret' },
|
|
});
|
|
|
|
const summary = summarizeRunToolBundle(bundle);
|
|
expect(summary).toEqual({
|
|
mcpServers: [
|
|
{
|
|
id: 'local-tools',
|
|
label: 'Local tools',
|
|
transport: 'stdio',
|
|
enabled: true,
|
|
},
|
|
{
|
|
id: 'remote-tools',
|
|
transport: 'http',
|
|
enabled: true,
|
|
authMode: 'oauth',
|
|
},
|
|
],
|
|
});
|
|
expect(JSON.stringify(summary)).not.toContain('secret');
|
|
expect(JSON.stringify(summary)).not.toContain('server.js');
|
|
});
|
|
|
|
it('uses only run-scoped MCP servers in sandbox mode', () => {
|
|
const persistedServers = normalizeRunToolBundleForRun({
|
|
mcpServers: [
|
|
{
|
|
id: 'persisted',
|
|
transport: 'http',
|
|
url: 'https://persisted.example.test/mcp',
|
|
},
|
|
],
|
|
}).mcpServers;
|
|
const runScopedServers = normalizeRunToolBundleForRun({
|
|
mcpServers: [
|
|
{
|
|
id: 'run-only',
|
|
transport: 'stdio',
|
|
command: 'node',
|
|
args: ['run-tool.js'],
|
|
},
|
|
],
|
|
}).mcpServers;
|
|
|
|
const selection = resolveExternalMcpServersForRun({
|
|
persistedServers,
|
|
runScopedServers,
|
|
sandboxMode: true,
|
|
});
|
|
|
|
expect(selection.enabledServers.map((server) => server.id)).toEqual(['run-only']);
|
|
expect([...selection.persistedTokenServerIds]).toEqual([]);
|
|
});
|
|
|
|
it('rejects malformed run-scoped MCP server entries for request payloads', () => {
|
|
expect(parseRunToolBundleForRequest('bad')).toEqual({
|
|
ok: false,
|
|
message: 'toolBundle must be an object',
|
|
});
|
|
expect(parseRunToolBundleForRequest({ mcpServers: 'bad' })).toEqual({
|
|
ok: false,
|
|
message: 'toolBundle.mcpServers must be an array',
|
|
});
|
|
expect(parseRunToolBundleForRequest({
|
|
mcpServers: [
|
|
{
|
|
id: 'missing-command',
|
|
transport: 'stdio',
|
|
},
|
|
],
|
|
})).toEqual({
|
|
ok: false,
|
|
message: 'toolBundle.mcpServers[0] is invalid',
|
|
});
|
|
expect(parseRunToolBundleForRequest({
|
|
mcpServers: [
|
|
{
|
|
id: 'dup',
|
|
transport: 'stdio',
|
|
command: 'node',
|
|
},
|
|
{
|
|
id: 'dup',
|
|
transport: 'http',
|
|
url: 'https://example.test/mcp',
|
|
},
|
|
],
|
|
})).toEqual({
|
|
ok: false,
|
|
message: 'toolBundle.mcpServers[1] duplicates server id "dup"',
|
|
});
|
|
});
|
|
|
|
it('lets a run-scoped server override persisted config without inheriting persisted tokens', () => {
|
|
const persistedServers = normalizeRunToolBundleForRun({
|
|
mcpServers: [
|
|
{
|
|
id: 'shared',
|
|
transport: 'http',
|
|
url: 'https://persisted.example.test/mcp',
|
|
},
|
|
{
|
|
id: 'persisted-only',
|
|
transport: 'http',
|
|
url: 'https://persisted-only.example.test/mcp',
|
|
},
|
|
],
|
|
}).mcpServers;
|
|
const runScopedServers = normalizeRunToolBundleForRun({
|
|
mcpServers: [
|
|
{
|
|
id: 'shared',
|
|
transport: 'http',
|
|
url: 'https://run.example.test/mcp',
|
|
headers: { Authorization: 'Bearer run-token' },
|
|
},
|
|
],
|
|
}).mcpServers;
|
|
|
|
const selection = resolveExternalMcpServersForRun({
|
|
persistedServers,
|
|
runScopedServers,
|
|
sandboxMode: false,
|
|
});
|
|
|
|
expect(selection.enabledServers).toHaveLength(2);
|
|
expect(selection.enabledServers.find((server) => server.id === 'shared')).toMatchObject({
|
|
url: 'https://run.example.test/mcp',
|
|
});
|
|
expect([...selection.persistedTokenServerIds]).toEqual(['persisted-only']);
|
|
});
|
|
|
|
it('rejects bundles for runtimes that cannot receive the requested servers', () => {
|
|
const stdioOnly = normalizeRunToolBundleForRun({
|
|
mcpServers: [
|
|
{
|
|
id: 'local',
|
|
transport: 'stdio',
|
|
command: 'node',
|
|
},
|
|
],
|
|
});
|
|
const remote = normalizeRunToolBundleForRun({
|
|
mcpServers: [
|
|
{
|
|
id: 'remote',
|
|
transport: 'http',
|
|
url: 'https://example.test/mcp',
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(validateRunToolBundleForAgent(stdioOnly, {
|
|
id: 'codex',
|
|
name: 'Codex CLI',
|
|
})).toEqual({
|
|
ok: false,
|
|
message: 'Codex CLI (codex) does not support run-scoped MCP tool bundles',
|
|
});
|
|
|
|
expect(validateRunToolBundleForAgent(remote, {
|
|
id: 'hermes',
|
|
name: 'Hermes',
|
|
externalMcpInjection: 'acp-merge',
|
|
})).toEqual({
|
|
ok: false,
|
|
message:
|
|
'toolBundle.mcpServers[0] uses http transport, but Hermes (hermes) only supports stdio run-scoped MCP servers',
|
|
});
|
|
|
|
expect(validateRunToolBundleForAgent(remote, {
|
|
id: 'claude',
|
|
name: 'Claude Code',
|
|
externalMcpInjection: 'claude-mcp-json',
|
|
})).toEqual({ ok: true });
|
|
|
|
expect(validateRunToolBundleForAgent(remote, {
|
|
id: 'claude',
|
|
name: 'Claude Code',
|
|
externalMcpInjection: 'claude-mcp-json',
|
|
}, {
|
|
deliveryTarget: 'external-project',
|
|
})).toEqual({
|
|
ok: false,
|
|
message:
|
|
'Claude Code (claude) receives run-scoped MCP tool bundles through project .mcp.json, ' +
|
|
'so toolBundle requires a daemon-managed project',
|
|
});
|
|
});
|
|
});
|