@ruifung/codemode-bridge 1.0.3-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +378 -0
- package/dist/cli/commands.d.ts +70 -0
- package/dist/cli/commands.js +436 -0
- package/dist/cli/config-manager.d.ts +53 -0
- package/dist/cli/config-manager.js +142 -0
- package/dist/cli/index.d.ts +19 -0
- package/dist/cli/index.js +165 -0
- package/dist/executor/container-executor.d.ts +81 -0
- package/dist/executor/container-executor.js +351 -0
- package/dist/executor/executor-test-suite.d.ts +22 -0
- package/dist/executor/executor-test-suite.js +395 -0
- package/dist/executor/isolated-vm-executor.d.ts +78 -0
- package/dist/executor/isolated-vm-executor.js +368 -0
- package/dist/executor/vm2-executor.d.ts +21 -0
- package/dist/executor/vm2-executor.js +109 -0
- package/dist/executor/wrap-code.d.ts +52 -0
- package/dist/executor/wrap-code.js +80 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/mcp/config.d.ts +44 -0
- package/dist/mcp/config.js +102 -0
- package/dist/mcp/e2e-bridge-test-suite.d.ts +28 -0
- package/dist/mcp/e2e-bridge-test-suite.js +429 -0
- package/dist/mcp/executor.d.ts +31 -0
- package/dist/mcp/executor.js +121 -0
- package/dist/mcp/mcp-adapter.d.ts +12 -0
- package/dist/mcp/mcp-adapter.js +49 -0
- package/dist/mcp/mcp-client.d.ts +85 -0
- package/dist/mcp/mcp-client.js +441 -0
- package/dist/mcp/oauth-handler.d.ts +33 -0
- package/dist/mcp/oauth-handler.js +138 -0
- package/dist/mcp/server.d.ts +25 -0
- package/dist/mcp/server.js +322 -0
- package/dist/mcp/token-persistence.d.ts +57 -0
- package/dist/mcp/token-persistence.js +131 -0
- package/dist/utils/logger.d.ts +44 -0
- package/dist/utils/logger.js +123 -0
- package/package.json +56 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Loader - Load MCP server configurations from files
|
|
3
|
+
* Supports VS Code's mcp.json format and other config files
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
/**
|
|
8
|
+
* Load MCP server configurations from VS Code's mcp.json file
|
|
9
|
+
* Default location: ~/.config/Code/User/mcp.json (Linux/Mac) or
|
|
10
|
+
* %APPDATA%\Code\User\mcp.json (Windows)
|
|
11
|
+
*/
|
|
12
|
+
export function loadMCPConfigFile(configPath) {
|
|
13
|
+
let resolvedPath = configPath;
|
|
14
|
+
if (!resolvedPath) {
|
|
15
|
+
// Determine default config path based on platform
|
|
16
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || process.cwd();
|
|
17
|
+
if (process.platform === "win32") {
|
|
18
|
+
resolvedPath = path.join(homeDir, "AppData", "Roaming", "Code", "User", "mcp.json");
|
|
19
|
+
}
|
|
20
|
+
else if (process.platform === "darwin") {
|
|
21
|
+
resolvedPath = path.join(homeDir, "Library", "Application Support", "Code", "User", "mcp.json");
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
// Linux and others
|
|
25
|
+
resolvedPath = path.join(homeDir, ".config", "Code", "User", "mcp.json");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
29
|
+
throw new Error(`MCP config file not found at: ${resolvedPath}`);
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const content = fs.readFileSync(resolvedPath, "utf-8");
|
|
33
|
+
return JSON.parse(content);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw new Error(`Failed to parse MCP config file at ${resolvedPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get a list of all configured server names from the config file
|
|
41
|
+
*/
|
|
42
|
+
export function getServerNames(config) {
|
|
43
|
+
return Object.keys(config.servers || {});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Get a server configuration by name
|
|
47
|
+
*/
|
|
48
|
+
export function getServerConfig(config, serverName) {
|
|
49
|
+
const entry = config.servers?.[serverName];
|
|
50
|
+
if (!entry) {
|
|
51
|
+
throw new Error(`Server "${serverName}" not found in MCP config`);
|
|
52
|
+
}
|
|
53
|
+
// Convert from config entry to MCPServerConfig
|
|
54
|
+
const serverConfig = {
|
|
55
|
+
name: serverName,
|
|
56
|
+
type: entry.type,
|
|
57
|
+
};
|
|
58
|
+
if (entry.type === "stdio") {
|
|
59
|
+
if (!entry.command) {
|
|
60
|
+
throw new Error(`Server "${serverName}" of type "stdio" requires "command" field`);
|
|
61
|
+
}
|
|
62
|
+
serverConfig.command = entry.command;
|
|
63
|
+
serverConfig.args = entry.args;
|
|
64
|
+
serverConfig.env = entry.env;
|
|
65
|
+
}
|
|
66
|
+
else if (entry.type === "http") {
|
|
67
|
+
if (!entry.url) {
|
|
68
|
+
throw new Error(`Server "${serverName}" of type "http" requires "url" field`);
|
|
69
|
+
}
|
|
70
|
+
serverConfig.url = entry.url;
|
|
71
|
+
// Extract oauth configuration if present
|
|
72
|
+
if (entry.oauth) {
|
|
73
|
+
serverConfig.oauth = entry.oauth;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return serverConfig;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Get multiple server configs by name from the config file
|
|
80
|
+
*/
|
|
81
|
+
export function getServerConfigs(config, serverNames) {
|
|
82
|
+
return serverNames.map((name) => getServerConfig(config, name));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Load and return all healthy servers from the config
|
|
86
|
+
* (filters out offline servers based on availability checks)
|
|
87
|
+
*/
|
|
88
|
+
export async function loadAvailableServers(config, serverNames) {
|
|
89
|
+
const names = serverNames || getServerNames(config);
|
|
90
|
+
const available = [];
|
|
91
|
+
const unavailable = [];
|
|
92
|
+
for (const name of names) {
|
|
93
|
+
try {
|
|
94
|
+
const serverConfig = getServerConfig(config, name);
|
|
95
|
+
available.push(serverConfig);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
unavailable.push(name);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { available, unavailable };
|
|
102
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E Bridge Test Suite Factory
|
|
3
|
+
*
|
|
4
|
+
* Reusable test suite that verifies the full codemode bridge pipeline
|
|
5
|
+
* against any Executor implementation:
|
|
6
|
+
*
|
|
7
|
+
* [Downstream MCP Client]
|
|
8
|
+
* ↔ InMemoryTransport
|
|
9
|
+
* ↔ [McpServer with codemode tool]
|
|
10
|
+
* ↔ Executor (parameterized)
|
|
11
|
+
* ↔ [Mock upstream MCP tools]
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { createE2EBridgeTestSuite } from './e2e-bridge-test-suite.js';
|
|
16
|
+
* import { createVM2Executor } from '../executor/vm2-executor.js';
|
|
17
|
+
*
|
|
18
|
+
* createE2EBridgeTestSuite('vm2', () => new VM2Executor(10000));
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
import type { Executor } from '@cloudflare/codemode';
|
|
22
|
+
export interface E2EBridgeTestSuiteOptions {
|
|
23
|
+
/** Test names to skip (exact match against it() description) */
|
|
24
|
+
skipTests?: string[];
|
|
25
|
+
/** Per-test timeout in ms (default: vitest default) */
|
|
26
|
+
testTimeout?: number;
|
|
27
|
+
}
|
|
28
|
+
export declare function createE2EBridgeTestSuite(executorName: string, createExecutor: () => Executor, options?: E2EBridgeTestSuiteOptions): void;
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E Bridge Test Suite Factory
|
|
3
|
+
*
|
|
4
|
+
* Reusable test suite that verifies the full codemode bridge pipeline
|
|
5
|
+
* against any Executor implementation:
|
|
6
|
+
*
|
|
7
|
+
* [Downstream MCP Client]
|
|
8
|
+
* ↔ InMemoryTransport
|
|
9
|
+
* ↔ [McpServer with codemode tool]
|
|
10
|
+
* ↔ Executor (parameterized)
|
|
11
|
+
* ↔ [Mock upstream MCP tools]
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { createE2EBridgeTestSuite } from './e2e-bridge-test-suite.js';
|
|
16
|
+
* import { createVM2Executor } from '../executor/vm2-executor.js';
|
|
17
|
+
*
|
|
18
|
+
* createE2EBridgeTestSuite('vm2', () => new VM2Executor(10000));
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
22
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
23
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
24
|
+
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
|
|
25
|
+
import { createCodeTool } from '@cloudflare/codemode/ai';
|
|
26
|
+
import { z } from 'zod';
|
|
27
|
+
import { adaptAISDKToolToMCP } from './mcp-adapter.js';
|
|
28
|
+
import { jsonSchemaToZod } from './server.js';
|
|
29
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
30
|
+
/**
|
|
31
|
+
* Create a mock upstream MCP server with test tools and connect an MCP client
|
|
32
|
+
* to it via InMemoryTransport.
|
|
33
|
+
*/
|
|
34
|
+
async function createMockUpstreamServer() {
|
|
35
|
+
const upstream = new McpServer({ name: 'test-upstream', version: '1.0.0' });
|
|
36
|
+
upstream.registerTool('add', {
|
|
37
|
+
description: 'Add two numbers',
|
|
38
|
+
inputSchema: z.object({ a: z.number(), b: z.number() }).strict(),
|
|
39
|
+
}, async ({ a, b }) => ({
|
|
40
|
+
content: [{ type: 'text', text: JSON.stringify(a + b) }],
|
|
41
|
+
}));
|
|
42
|
+
upstream.registerTool('echo', {
|
|
43
|
+
description: 'Echo back the input text',
|
|
44
|
+
inputSchema: z.object({ text: z.string() }).strict(),
|
|
45
|
+
}, async ({ text }) => ({
|
|
46
|
+
content: [{ type: 'text', text }],
|
|
47
|
+
}));
|
|
48
|
+
upstream.registerTool('get_user', {
|
|
49
|
+
description: 'Get user by ID',
|
|
50
|
+
inputSchema: z.object({ id: z.number() }).strict(),
|
|
51
|
+
}, async ({ id }) => ({
|
|
52
|
+
content: [
|
|
53
|
+
{
|
|
54
|
+
type: 'text',
|
|
55
|
+
text: JSON.stringify({ id, name: `User ${id}`, email: `user${id}@test.com` }),
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
}));
|
|
59
|
+
upstream.registerTool('fail', {
|
|
60
|
+
description: 'A tool that always fails',
|
|
61
|
+
inputSchema: z.object({ message: z.string().optional() }).strict(),
|
|
62
|
+
}, async ({ message }) => {
|
|
63
|
+
throw new Error(message ?? 'Intentional failure');
|
|
64
|
+
});
|
|
65
|
+
upstream.registerTool('multiply', {
|
|
66
|
+
description: 'Multiply two numbers',
|
|
67
|
+
inputSchema: z.object({ a: z.number(), b: z.number() }).strict(),
|
|
68
|
+
}, async ({ a, b }) => ({
|
|
69
|
+
content: [{ type: 'text', text: JSON.stringify(a * b) }],
|
|
70
|
+
}));
|
|
71
|
+
upstream.registerTool('list_items', {
|
|
72
|
+
description: 'Return a list of items',
|
|
73
|
+
inputSchema: z.object({ count: z.number() }).strict(),
|
|
74
|
+
}, async ({ count }) => ({
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: 'text',
|
|
78
|
+
text: JSON.stringify(Array.from({ length: count }, (_, i) => ({ id: i + 1, name: `Item ${i + 1}` }))),
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
}));
|
|
82
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
83
|
+
const upstreamClient = new Client({ name: 'bridge-test-client', version: '1.0.0' }, { capabilities: {} });
|
|
84
|
+
await upstream.connect(serverTransport);
|
|
85
|
+
await upstreamClient.connect(clientTransport);
|
|
86
|
+
return { upstream, upstreamClient, clientTransport, serverTransport };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Create the full codemode bridge pipeline with a given executor and return a
|
|
90
|
+
* downstream MCP client that can call the codemode tool.
|
|
91
|
+
*/
|
|
92
|
+
async function createBridgePipeline(upstreamClient, serverName, executor) {
|
|
93
|
+
// 1. List tools from upstream (native MCP format)
|
|
94
|
+
const { tools: upstreamTools } = await upstreamClient.listTools();
|
|
95
|
+
// 2. Convert to ToolDescriptor format with execute functions
|
|
96
|
+
const toolDescriptors = {};
|
|
97
|
+
for (const tool of upstreamTools) {
|
|
98
|
+
const namespacedName = `${serverName}__${tool.name}`;
|
|
99
|
+
toolDescriptors[namespacedName] = {
|
|
100
|
+
description: tool.description || '',
|
|
101
|
+
inputSchema: jsonSchemaToZod(tool.inputSchema),
|
|
102
|
+
execute: async (args) => {
|
|
103
|
+
const result = await upstreamClient.callTool({ name: tool.name, arguments: args });
|
|
104
|
+
return result;
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// 3. Create the codemode tool via SDK
|
|
109
|
+
const codemodeTool = createCodeTool({ tools: toolDescriptors, executor });
|
|
110
|
+
// 4. Create bridge MCP server and register codemode tool
|
|
111
|
+
const bridgeServer = new McpServer({ name: 'codemode-bridge-test', version: '1.0.0' });
|
|
112
|
+
await adaptAISDKToolToMCP(bridgeServer, codemodeTool);
|
|
113
|
+
// 5. Wire up downstream client via InMemoryTransport
|
|
114
|
+
const [downstreamClientTransport, downstreamServerTransport] = InMemoryTransport.createLinkedPair();
|
|
115
|
+
const downstreamClient = new Client({ name: 'test-consumer', version: '1.0.0' }, { capabilities: {} });
|
|
116
|
+
await bridgeServer.connect(downstreamServerTransport);
|
|
117
|
+
await downstreamClient.connect(downstreamClientTransport);
|
|
118
|
+
return { bridgeServer, downstreamClient, downstreamClientTransport, downstreamServerTransport, _executor: executor };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Helper: call the codemode tool and parse the result
|
|
122
|
+
*/
|
|
123
|
+
async function callCodemode(client, code) {
|
|
124
|
+
const response = await client.callTool({ name: 'eval', arguments: { code } });
|
|
125
|
+
const content = response.content;
|
|
126
|
+
if (!content || !Array.isArray(content) || content.length === 0) {
|
|
127
|
+
throw new Error(`Unexpected response shape: ${JSON.stringify(response)}`);
|
|
128
|
+
}
|
|
129
|
+
const text = content[0].text;
|
|
130
|
+
return JSON.parse(text);
|
|
131
|
+
}
|
|
132
|
+
export function createE2EBridgeTestSuite(executorName, createExecutor, options) {
|
|
133
|
+
const skipSet = new Set(options?.skipTests ?? []);
|
|
134
|
+
/** Use it.skip for tests in the skip list, it otherwise */
|
|
135
|
+
const testOrSkip = (testName, fn) => {
|
|
136
|
+
if (skipSet.has(testName)) {
|
|
137
|
+
it.skip(testName, fn);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
it(testName, fn, options?.testTimeout);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
describe(`E2E Bridge Pipeline [${executorName}]`, () => {
|
|
144
|
+
let upstreamState;
|
|
145
|
+
let bridgeState;
|
|
146
|
+
let client;
|
|
147
|
+
beforeAll(async () => {
|
|
148
|
+
upstreamState = await createMockUpstreamServer();
|
|
149
|
+
const executor = createExecutor();
|
|
150
|
+
// If executor has a lazy init (e.g. container), trigger it now so the
|
|
151
|
+
// startup cost is not counted against the first test's timeout.
|
|
152
|
+
if ('init' in executor && typeof executor.init === 'function') {
|
|
153
|
+
await executor.init();
|
|
154
|
+
}
|
|
155
|
+
bridgeState = await createBridgePipeline(upstreamState.upstreamClient, 'test', executor);
|
|
156
|
+
client = bridgeState.downstreamClient;
|
|
157
|
+
}, options?.testTimeout ? options.testTimeout * 2 : undefined);
|
|
158
|
+
afterAll(async () => {
|
|
159
|
+
await bridgeState.downstreamClient.close();
|
|
160
|
+
await bridgeState.bridgeServer.close();
|
|
161
|
+
await upstreamState.upstreamClient.close();
|
|
162
|
+
await upstreamState.upstream.close();
|
|
163
|
+
// Cleanup executor if it has a dispose method
|
|
164
|
+
const executor = bridgeState._executor;
|
|
165
|
+
if (executor && 'dispose' in executor && typeof executor.dispose === 'function') {
|
|
166
|
+
await Promise.resolve(executor.dispose());
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
// ── Tool Discovery ──────────────────────────────────────────────────
|
|
170
|
+
describe('Tool Discovery', () => {
|
|
171
|
+
testOrSkip('should expose a single codemode tool', async () => {
|
|
172
|
+
const { tools } = await client.listTools();
|
|
173
|
+
expect(tools).toHaveLength(1);
|
|
174
|
+
expect(tools[0].name).toBe('eval');
|
|
175
|
+
});
|
|
176
|
+
testOrSkip('should have code input schema', async () => {
|
|
177
|
+
const { tools } = await client.listTools();
|
|
178
|
+
const codemodeTool = tools[0];
|
|
179
|
+
expect(codemodeTool.inputSchema).toBeDefined();
|
|
180
|
+
expect(codemodeTool.inputSchema.properties).toHaveProperty('code');
|
|
181
|
+
});
|
|
182
|
+
testOrSkip('should include tool descriptions from upstream', async () => {
|
|
183
|
+
const { tools } = await client.listTools();
|
|
184
|
+
const description = tools[0].description || '';
|
|
185
|
+
expect(description).toContain('test__add');
|
|
186
|
+
expect(description).toContain('test__echo');
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
// ── Simple Code Execution ───────────────────────────────────────────
|
|
190
|
+
describe('Simple Code Execution', () => {
|
|
191
|
+
testOrSkip('should execute arithmetic and return result', async () => {
|
|
192
|
+
const output = await callCodemode(client, 'async () => { return 1 + 2; }');
|
|
193
|
+
expect(output.result).toBe(3);
|
|
194
|
+
});
|
|
195
|
+
testOrSkip('should execute string operations', async () => {
|
|
196
|
+
const output = await callCodemode(client, 'async () => { return "hello" + " " + "world"; }');
|
|
197
|
+
expect(output.result).toBe('hello world');
|
|
198
|
+
});
|
|
199
|
+
testOrSkip('should execute object construction', async () => {
|
|
200
|
+
const output = await callCodemode(client, 'async () => { return { a: 1, b: [2, 3] }; }');
|
|
201
|
+
expect(output.result).toEqual({ a: 1, b: [2, 3] });
|
|
202
|
+
});
|
|
203
|
+
testOrSkip('should handle async/await code', async () => {
|
|
204
|
+
const output = await callCodemode(client, 'async () => { const p = Promise.resolve(42); return await p; }');
|
|
205
|
+
expect(output.result).toBe(42);
|
|
206
|
+
});
|
|
207
|
+
testOrSkip('should echo back the original code', async () => {
|
|
208
|
+
const code = 'async () => { return 99; }';
|
|
209
|
+
const output = await callCodemode(client, code);
|
|
210
|
+
expect(output.code).toBe(code);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
// ── Tool Invocation Through Bridge ──────────────────────────────────
|
|
214
|
+
describe('Tool Invocation', () => {
|
|
215
|
+
testOrSkip('should call upstream add tool and return result', async () => {
|
|
216
|
+
const output = await callCodemode(client, 'async () => { const r = await codemode.test__add({ a: 5, b: 3 }); return r; }');
|
|
217
|
+
expect(output.result).toBeDefined();
|
|
218
|
+
const content = output.result?.content?.[0]?.text;
|
|
219
|
+
expect(content).toBeDefined();
|
|
220
|
+
expect(JSON.parse(content)).toBe(8);
|
|
221
|
+
});
|
|
222
|
+
testOrSkip('should call upstream echo tool', async () => {
|
|
223
|
+
const output = await callCodemode(client, 'async () => { const r = await codemode.test__echo({ text: "hello bridge" }); return r; }');
|
|
224
|
+
const content = output.result?.content?.[0]?.text;
|
|
225
|
+
expect(content).toBe('hello bridge');
|
|
226
|
+
});
|
|
227
|
+
testOrSkip('should call upstream get_user tool with structured response', async () => {
|
|
228
|
+
const output = await callCodemode(client, 'async () => { const r = await codemode.test__get_user({ id: 42 }); return JSON.parse(r.content[0].text); }');
|
|
229
|
+
expect(output.result).toEqual({ id: 42, name: 'User 42', email: 'user42@test.com' });
|
|
230
|
+
});
|
|
231
|
+
testOrSkip('should call multiple tools sequentially', async () => {
|
|
232
|
+
const output = await callCodemode(client, `async () => {
|
|
233
|
+
const sum = await codemode.test__add({ a: 10, b: 20 });
|
|
234
|
+
const product = await codemode.test__multiply({ a: 3, b: 7 });
|
|
235
|
+
return {
|
|
236
|
+
sum: JSON.parse(sum.content[0].text),
|
|
237
|
+
product: JSON.parse(product.content[0].text),
|
|
238
|
+
};
|
|
239
|
+
}`);
|
|
240
|
+
expect(output.result).toEqual({ sum: 30, product: 21 });
|
|
241
|
+
});
|
|
242
|
+
testOrSkip('should call tools in parallel with Promise.all', async () => {
|
|
243
|
+
const output = await callCodemode(client, `async () => {
|
|
244
|
+
const [r1, r2, r3] = await Promise.all([
|
|
245
|
+
codemode.test__add({ a: 1, b: 1 }),
|
|
246
|
+
codemode.test__add({ a: 2, b: 2 }),
|
|
247
|
+
codemode.test__add({ a: 3, b: 3 }),
|
|
248
|
+
]);
|
|
249
|
+
return [
|
|
250
|
+
JSON.parse(r1.content[0].text),
|
|
251
|
+
JSON.parse(r2.content[0].text),
|
|
252
|
+
JSON.parse(r3.content[0].text),
|
|
253
|
+
];
|
|
254
|
+
}`);
|
|
255
|
+
expect(output.result).toEqual([2, 4, 6]);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
// ── Tool Chaining & Data Flow ───────────────────────────────────────
|
|
259
|
+
describe('Tool Chaining', () => {
|
|
260
|
+
testOrSkip('should chain tool outputs as inputs to subsequent tool calls', async () => {
|
|
261
|
+
const output = await callCodemode(client, `async () => {
|
|
262
|
+
const sumResult = await codemode.test__add({ a: 5, b: 10 });
|
|
263
|
+
const sum = JSON.parse(sumResult.content[0].text);
|
|
264
|
+
const doubled = await codemode.test__multiply({ a: sum, b: 2 });
|
|
265
|
+
return JSON.parse(doubled.content[0].text);
|
|
266
|
+
}`);
|
|
267
|
+
expect(output.result).toBe(30); // (5 + 10) * 2
|
|
268
|
+
});
|
|
269
|
+
testOrSkip('should iterate over tool results', async () => {
|
|
270
|
+
const output = await callCodemode(client, `async () => {
|
|
271
|
+
const itemsResult = await codemode.test__list_items({ count: 3 });
|
|
272
|
+
const items = JSON.parse(itemsResult.content[0].text);
|
|
273
|
+
const names = items.map(item => item.name);
|
|
274
|
+
return names;
|
|
275
|
+
}`);
|
|
276
|
+
expect(output.result).toEqual(['Item 1', 'Item 2', 'Item 3']);
|
|
277
|
+
});
|
|
278
|
+
testOrSkip('should aggregate results from multiple tool calls', async () => {
|
|
279
|
+
const output = await callCodemode(client, `async () => {
|
|
280
|
+
let total = 0;
|
|
281
|
+
for (let i = 1; i <= 4; i++) {
|
|
282
|
+
const r = await codemode.test__multiply({ a: i, b: i });
|
|
283
|
+
total += JSON.parse(r.content[0].text);
|
|
284
|
+
}
|
|
285
|
+
return total;
|
|
286
|
+
}`);
|
|
287
|
+
// 1*1 + 2*2 + 3*3 + 4*4 = 1 + 4 + 9 + 16 = 30
|
|
288
|
+
expect(output.result).toBe(30);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
// ── Error Handling ──────────────────────────────────────────────────
|
|
292
|
+
describe('Error Handling', () => {
|
|
293
|
+
testOrSkip('should handle code execution errors gracefully', async () => {
|
|
294
|
+
const response = await client.callTool({
|
|
295
|
+
name: 'eval',
|
|
296
|
+
arguments: { code: 'async () => { throw new Error("boom"); }' },
|
|
297
|
+
});
|
|
298
|
+
const text = response.content?.[0]?.text || '';
|
|
299
|
+
expect(text).toContain('boom');
|
|
300
|
+
});
|
|
301
|
+
testOrSkip('should handle syntax errors in user code', async () => {
|
|
302
|
+
const response = await client.callTool({
|
|
303
|
+
name: 'eval',
|
|
304
|
+
arguments: { code: 'async () => { this is not valid javascript!!! }' },
|
|
305
|
+
});
|
|
306
|
+
const text = response.content?.[0]?.text || '';
|
|
307
|
+
expect(text.length).toBeGreaterThan(0);
|
|
308
|
+
});
|
|
309
|
+
testOrSkip('should return error response from upstream tool (does not throw)', async () => {
|
|
310
|
+
// MCP SDK's callTool does NOT throw on upstream tool errors — it returns
|
|
311
|
+
// an error response object. So the sandbox code receives the response,
|
|
312
|
+
// not an exception.
|
|
313
|
+
const output = await callCodemode(client, `async () => {
|
|
314
|
+
const r = await codemode.test__fail({ message: "test error" });
|
|
315
|
+
return r;
|
|
316
|
+
}`);
|
|
317
|
+
// The response should contain the error information
|
|
318
|
+
expect(output.result).toBeDefined();
|
|
319
|
+
// MCP error responses have isError: true and content with the error text
|
|
320
|
+
const content = output.result?.content?.[0]?.text;
|
|
321
|
+
expect(content).toBeDefined();
|
|
322
|
+
expect(content.toLowerCase()).toContain('error');
|
|
323
|
+
});
|
|
324
|
+
testOrSkip('should handle calling non-existent tools', async () => {
|
|
325
|
+
const response = await client.callTool({
|
|
326
|
+
name: 'eval',
|
|
327
|
+
arguments: { code: 'async () => { return await codemode.test__nonexistent({}); }' },
|
|
328
|
+
});
|
|
329
|
+
const text = response.content?.[0]?.text || '';
|
|
330
|
+
expect(text.toLowerCase()).toMatch(/error|not found|not defined|not a function/);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
// ── Console Logging ─────────────────────────────────────────────────
|
|
334
|
+
describe('Console Logging', () => {
|
|
335
|
+
testOrSkip('should capture console.log output', async () => {
|
|
336
|
+
const output = await callCodemode(client, `async () => {
|
|
337
|
+
console.log("hello from sandbox");
|
|
338
|
+
return "done";
|
|
339
|
+
}`);
|
|
340
|
+
expect(output.result).toBe('done');
|
|
341
|
+
expect(output.logs).toBeDefined();
|
|
342
|
+
expect(output.logs).toContain('hello from sandbox');
|
|
343
|
+
});
|
|
344
|
+
testOrSkip('should capture multiple log lines', async () => {
|
|
345
|
+
const output = await callCodemode(client, `async () => {
|
|
346
|
+
console.log("line 1");
|
|
347
|
+
console.log("line 2");
|
|
348
|
+
console.log("line 3");
|
|
349
|
+
return 42;
|
|
350
|
+
}`);
|
|
351
|
+
expect(output.logs).toHaveLength(3);
|
|
352
|
+
expect(output.logs).toContain('line 1');
|
|
353
|
+
expect(output.logs).toContain('line 2');
|
|
354
|
+
expect(output.logs).toContain('line 3');
|
|
355
|
+
});
|
|
356
|
+
testOrSkip('should not include logs key when no console output', async () => {
|
|
357
|
+
const output = await callCodemode(client, 'async () => { return 1; }');
|
|
358
|
+
expect(output.logs).toBeUndefined();
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
// ── Code Normalization ──────────────────────────────────────────────
|
|
362
|
+
describe('Code Normalization', () => {
|
|
363
|
+
testOrSkip('should handle bare return statements (auto-wrapped)', async () => {
|
|
364
|
+
const output = await callCodemode(client, 'return 42;');
|
|
365
|
+
expect(output.result).toBe(42);
|
|
366
|
+
});
|
|
367
|
+
testOrSkip('should handle expression-only code', async () => {
|
|
368
|
+
const output = await callCodemode(client, '1 + 2 + 3');
|
|
369
|
+
expect(output.result).toBe(6);
|
|
370
|
+
});
|
|
371
|
+
testOrSkip('should handle arrow function without async', async () => {
|
|
372
|
+
const output = await callCodemode(client, '() => { return 7; }');
|
|
373
|
+
expect(output.result).toBe(7);
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
// ── Isolation & Safety ──────────────────────────────────────────────
|
|
377
|
+
describe('Isolation', () => {
|
|
378
|
+
testOrSkip('should not allow require access', async () => {
|
|
379
|
+
const response = await client.callTool({
|
|
380
|
+
name: 'eval',
|
|
381
|
+
arguments: { code: 'async () => { return require("fs"); }' },
|
|
382
|
+
});
|
|
383
|
+
const text = response.content?.[0]?.text || '';
|
|
384
|
+
expect(text.toLowerCase()).toMatch(/error|not defined|not allowed/);
|
|
385
|
+
});
|
|
386
|
+
testOrSkip('should not allow process access', async () => {
|
|
387
|
+
const response = await client.callTool({
|
|
388
|
+
name: 'eval',
|
|
389
|
+
arguments: { code: 'async () => { return process.env; }' },
|
|
390
|
+
});
|
|
391
|
+
const text = response.content?.[0]?.text || '';
|
|
392
|
+
expect(text.toLowerCase()).toMatch(/error|not defined|not allowed/);
|
|
393
|
+
});
|
|
394
|
+
testOrSkip('should isolate state between executions', async () => {
|
|
395
|
+
await callCodemode(client, 'async () => { globalThis.__test = 123; return "set"; }');
|
|
396
|
+
const output = await callCodemode(client, 'async () => { return typeof globalThis.__test; }');
|
|
397
|
+
expect(output.result).toBe('undefined');
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
// ── Response Format ─────────────────────────────────────────────────
|
|
401
|
+
describe('Response Format', () => {
|
|
402
|
+
testOrSkip('should return well-formed MCP content', async () => {
|
|
403
|
+
const response = await client.callTool({
|
|
404
|
+
name: 'eval',
|
|
405
|
+
arguments: { code: 'async () => { return { answer: 42 }; }' },
|
|
406
|
+
});
|
|
407
|
+
expect(response).toHaveProperty('content');
|
|
408
|
+
const content = response.content;
|
|
409
|
+
expect(Array.isArray(content)).toBe(true);
|
|
410
|
+
expect(content.length).toBeGreaterThan(0);
|
|
411
|
+
expect(content[0]).toHaveProperty('type', 'text');
|
|
412
|
+
expect(content[0]).toHaveProperty('text');
|
|
413
|
+
const parsed = JSON.parse(content[0].text);
|
|
414
|
+
expect(parsed).toHaveProperty('code');
|
|
415
|
+
expect(parsed).toHaveProperty('result');
|
|
416
|
+
expect(parsed.result).toEqual({ answer: 42 });
|
|
417
|
+
});
|
|
418
|
+
testOrSkip('should include code field echoing the input', async () => {
|
|
419
|
+
const code = 'async () => { return "test"; }';
|
|
420
|
+
const response = await client.callTool({
|
|
421
|
+
name: 'eval',
|
|
422
|
+
arguments: { code },
|
|
423
|
+
});
|
|
424
|
+
const parsed = JSON.parse(response.content[0].text);
|
|
425
|
+
expect(parsed.code).toBe(code);
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executor factory and registry for Codemode SDK
|
|
3
|
+
* Selects the best available executor (isolated-vm → container → vm2)
|
|
4
|
+
*/
|
|
5
|
+
import type { Executor } from "@cloudflare/codemode";
|
|
6
|
+
export type ExecutorType = 'isolated-vm' | 'container' | 'vm2';
|
|
7
|
+
/**
|
|
8
|
+
* Metadata about the executor that was created.
|
|
9
|
+
*/
|
|
10
|
+
export interface ExecutorInfo {
|
|
11
|
+
/** The executor type that was selected */
|
|
12
|
+
type: ExecutorType;
|
|
13
|
+
/** How the executor was selected */
|
|
14
|
+
reason: 'explicit' | 'auto-detected';
|
|
15
|
+
/** Execution timeout in ms */
|
|
16
|
+
timeout: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Factory function to create an Executor instance.
|
|
20
|
+
*
|
|
21
|
+
* Selection logic:
|
|
22
|
+
* - If EXECUTOR_TYPE is set, that executor is used (throws if unavailable).
|
|
23
|
+
* - Otherwise, executors are tried in preference order (isolated-vm →
|
|
24
|
+
* container → vm2) and the first available one is selected.
|
|
25
|
+
*
|
|
26
|
+
* Returns both the executor and metadata about the selection.
|
|
27
|
+
*/
|
|
28
|
+
export declare function createExecutor(timeout?: number): Promise<{
|
|
29
|
+
executor: Executor;
|
|
30
|
+
info: ExecutorInfo;
|
|
31
|
+
}>;
|