070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
232 lines
6.2 KiB
TypeScript
232 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',
|
|
});
|
|
});
|
|
});
|