@ruifung/codemode-bridge 1.0.6 ā 1.0.8
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/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +6 -4
- package/dist/cli/config-manager.js +2 -2
- package/dist/cli/index.js +3 -2
- package/dist/executor/container-executor.d.ts +7 -1
- package/dist/executor/container-executor.js +31 -3
- package/dist/executor/deno-executor.d.ts +26 -0
- package/dist/executor/deno-executor.js +251 -0
- package/dist/executor/executor-test-suite.js +9 -7
- package/dist/mcp/config.js +2 -2
- package/dist/mcp/e2e-bridge-test-suite.js +5 -3
- package/dist/mcp/executor.d.ts +3 -2
- package/dist/mcp/executor.js +68 -17
- package/dist/mcp/oauth-handler.js +2 -2
- package/dist/mcp/server.d.ts +3 -2
- package/dist/mcp/server.js +2 -2
- package/dist/mcp/token-persistence.js +3 -3
- package/dist/utils/env.d.ts +23 -0
- package/dist/utils/env.js +47 -0
- package/package.json +2 -2
package/dist/cli/commands.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
/**
|
|
7
7
|
* Run the bridge server
|
|
8
8
|
*/
|
|
9
|
-
export declare function runServer(configPath?: string, servers?: string[], debug?: boolean): Promise<void>;
|
|
9
|
+
export declare function runServer(configPath?: string, servers?: string[], debug?: boolean, executor?: string): Promise<void>;
|
|
10
10
|
/**
|
|
11
11
|
* List all configured servers
|
|
12
12
|
*/
|
package/dist/cli/commands.js
CHANGED
|
@@ -4,23 +4,25 @@
|
|
|
4
4
|
* Implements subcommands for managing and running the bridge
|
|
5
5
|
*/
|
|
6
6
|
import chalk from "chalk";
|
|
7
|
-
import * as fs from "fs";
|
|
8
|
-
import * as path from "path";
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
9
|
import { loadConfig, saveConfig, addServer, removeServer, updateServer, getServer, listServers, validateServer, getConfigFilePath, } from "./config-manager.js";
|
|
10
10
|
import { startCodeModeBridgeServer } from "../mcp/server.js";
|
|
11
11
|
import { getServerConfig } from "../mcp/config.js";
|
|
12
12
|
import { initializeLogger, logInfo, logError, flushStderrBuffer } from "../utils/logger.js";
|
|
13
|
+
import { getRuntimeName } from "../utils/env.js";
|
|
13
14
|
import { tokenPersistence } from "../mcp/token-persistence.js";
|
|
14
15
|
import { MCPClient } from "../mcp/mcp-client.js";
|
|
15
16
|
/**
|
|
16
17
|
* Run the bridge server
|
|
17
18
|
*/
|
|
18
|
-
export async function runServer(configPath, servers, debug) {
|
|
19
|
+
export async function runServer(configPath, servers, debug, executor) {
|
|
19
20
|
try {
|
|
20
21
|
// Initialize logger with debug mode if requested
|
|
21
22
|
initializeLogger(debug);
|
|
22
23
|
console.error(chalk.cyan("\nš Code Mode Bridge"));
|
|
23
24
|
console.error(chalk.cyan("====================\n"));
|
|
25
|
+
logInfo(`Runtime: ${getRuntimeName()}`, { component: 'CLI' });
|
|
24
26
|
// Load the bridge configuration
|
|
25
27
|
const bridgeConfig = loadConfig(configPath);
|
|
26
28
|
logInfo(`Loaded config from: ${getConfigFilePath(configPath)}`, { component: 'CLI' });
|
|
@@ -53,7 +55,7 @@ export async function runServer(configPath, servers, debug) {
|
|
|
53
55
|
}
|
|
54
56
|
logInfo(`Starting bridge with ${serverConfigs.length} server(s)`, { component: 'CLI' });
|
|
55
57
|
// Start the MCP bridge server
|
|
56
|
-
await startCodeModeBridgeServer(serverConfigs);
|
|
58
|
+
await startCodeModeBridgeServer(serverConfigs, executor);
|
|
57
59
|
// Flush buffered stderr output from stdio tools now that Bridge is fully running
|
|
58
60
|
flushStderrBuffer();
|
|
59
61
|
logInfo("Bridge is running!", { component: 'CLI' });
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Manages the bridge configuration stored in .config/codemode-bridge/mcp.json
|
|
5
5
|
* Provides utilities to load, save, and manipulate the configuration
|
|
6
6
|
*/
|
|
7
|
-
import * as fs from "fs";
|
|
8
|
-
import * as path from "path";
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
9
|
/**
|
|
10
10
|
* Get the default config directory for the current platform
|
|
11
11
|
*/
|
package/dist/cli/index.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import { Command } from "commander";
|
|
20
20
|
import { runServer, listServersCommand, showServerCommand, addServerCommand, removeServerCommand, editServerCommand, configInfoCommand, authLoginCommand, authLogoutCommand, authListCommand, } from "./commands.js";
|
|
21
21
|
import { getConfigFilePath } from "./config-manager.js";
|
|
22
|
-
import * as fs from "fs";
|
|
22
|
+
import * as fs from "node:fs";
|
|
23
23
|
const pkg = JSON.parse(fs.readFileSync(new URL("../../package.json", import.meta.url), "utf-8"));
|
|
24
24
|
const defaultConfigPath = getConfigFilePath();
|
|
25
25
|
const program = new Command();
|
|
@@ -34,9 +34,10 @@ program
|
|
|
34
34
|
.option("-c, --config <path>", `Path to mcp.json configuration file (default: ${defaultConfigPath})`)
|
|
35
35
|
.option("-s, --servers <names>", "Comma-separated list of servers to connect to")
|
|
36
36
|
.option("-d, --debug", "Enable debug logging")
|
|
37
|
+
.option("-e, --executor <type>", "Executor type (isolated-vm, container, deno, vm2)")
|
|
37
38
|
.action(async (options) => {
|
|
38
39
|
const servers = options.servers ? options.servers.split(",").map((s) => s.trim()) : undefined;
|
|
39
|
-
await runServer(options.config, servers, options.debug);
|
|
40
|
+
await runServer(options.config, servers, options.debug, options.executor);
|
|
40
41
|
});
|
|
41
42
|
// Config command group
|
|
42
43
|
const config = program.command("config").description("Manage bridge configuration").enablePositionalOptions();
|
|
@@ -18,8 +18,12 @@ import type { Executor, ExecuteResult } from '@cloudflare/codemode';
|
|
|
18
18
|
export interface ContainerExecutorOptions {
|
|
19
19
|
/** Execution timeout per call in ms (default 30000) */
|
|
20
20
|
timeout?: number;
|
|
21
|
-
/** Container image (default 'node:
|
|
21
|
+
/** Container image (default 'node:24-slim') */
|
|
22
22
|
image?: string;
|
|
23
|
+
/** Container user (default based on image) */
|
|
24
|
+
user?: string;
|
|
25
|
+
/** Container command to run the runner (default based on image) */
|
|
26
|
+
command?: string[];
|
|
23
27
|
/** Container runtime command ā 'docker' | 'podman' | auto-detect */
|
|
24
28
|
runtime?: string;
|
|
25
29
|
/** Memory limit (default '256m') */
|
|
@@ -30,6 +34,8 @@ export interface ContainerExecutorOptions {
|
|
|
30
34
|
export declare class ContainerExecutor implements Executor {
|
|
31
35
|
private runtime;
|
|
32
36
|
private image;
|
|
37
|
+
private containerUser;
|
|
38
|
+
private containerCommand;
|
|
33
39
|
private timeout;
|
|
34
40
|
private memoryLimit;
|
|
35
41
|
private cpuLimit;
|
|
@@ -20,6 +20,7 @@ import { createInterface } from 'node:readline';
|
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
21
|
import { dirname, join } from 'node:path';
|
|
22
22
|
import { wrapCode } from './wrap-code.js';
|
|
23
|
+
import { isBun, isDeno } from '../utils/env.js';
|
|
23
24
|
// āā Runtime detection āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
24
25
|
function detectRuntime(requested) {
|
|
25
26
|
if (requested)
|
|
@@ -67,10 +68,37 @@ export class ContainerExecutor {
|
|
|
67
68
|
this.pendingExecution = null;
|
|
68
69
|
this.initPromise = null;
|
|
69
70
|
this.timeout = options.timeout ?? 30000;
|
|
70
|
-
this.image = options.image ?? 'node:24-slim';
|
|
71
71
|
this.memoryLimit = options.memoryLimit ?? '256m';
|
|
72
72
|
this.cpuLimit = options.cpuLimit ?? 1.0;
|
|
73
73
|
this.runtime = detectRuntime(options.runtime);
|
|
74
|
+
// Platform-specific defaults
|
|
75
|
+
if (options.image) {
|
|
76
|
+
this.image = options.image;
|
|
77
|
+
this.containerUser = options.user ?? '1000';
|
|
78
|
+
this.containerCommand = options.command ?? ['node'];
|
|
79
|
+
}
|
|
80
|
+
else if (isBun()) {
|
|
81
|
+
this.image = 'oven/bun:debian';
|
|
82
|
+
this.containerUser = options.user ?? '1000'; // bun user is 1000
|
|
83
|
+
this.containerCommand = ['bun', 'run'];
|
|
84
|
+
}
|
|
85
|
+
else if (isDeno()) {
|
|
86
|
+
this.image = 'denoland/deno:debian';
|
|
87
|
+
this.containerUser = options.user ?? '1000'; // deno:debian has no 'deno' user by default
|
|
88
|
+
this.containerCommand = ['deno', 'run', '-A'];
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
this.image = 'node:24-slim';
|
|
92
|
+
this.containerUser = options.user ?? '1000'; // node user is 1000
|
|
93
|
+
this.containerCommand = ['node'];
|
|
94
|
+
}
|
|
95
|
+
// Start initialization immediately to create the container before the first execution.
|
|
96
|
+
if (process.env.DEBUG || process.env.NODE_ENV === 'test') {
|
|
97
|
+
console.log(`[ContainerExecutor] Using image: ${this.image}, user: ${this.containerUser}, command: ${this.containerCommand.join(' ')}`);
|
|
98
|
+
}
|
|
99
|
+
this.init().catch(err => {
|
|
100
|
+
process.stderr.write(`[container-executor] Immediate initialization failed: ${err.message}\n`);
|
|
101
|
+
});
|
|
74
102
|
}
|
|
75
103
|
/**
|
|
76
104
|
* Start the container and wait for the runner to signal readiness.
|
|
@@ -140,7 +168,7 @@ export class ContainerExecutor {
|
|
|
140
168
|
'--read-only', // immutable rootfs
|
|
141
169
|
'--tmpfs', '/tmp:rw,noexec,nosuid,size=64m', // writable /tmp
|
|
142
170
|
'--cap-drop=ALL', // drop all capabilities
|
|
143
|
-
'--user',
|
|
171
|
+
'--user', this.containerUser,
|
|
144
172
|
'--memory', this.memoryLimit,
|
|
145
173
|
`--cpus=${this.cpuLimit}`,
|
|
146
174
|
'--pids-limit=64', // limit process spawning
|
|
@@ -148,7 +176,7 @@ export class ContainerExecutor {
|
|
|
148
176
|
'-v', `${scripts.worker}:/app/container-worker.mjs:ro`, // mount worker script
|
|
149
177
|
'-w', '/app',
|
|
150
178
|
this.image,
|
|
151
|
-
|
|
179
|
+
...this.containerCommand, '/app/container-runner.mjs',
|
|
152
180
|
];
|
|
153
181
|
this.process = spawn(this.runtime, args, {
|
|
154
182
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Executor, ExecuteResult } from '@cloudflare/codemode';
|
|
2
|
+
export interface DenoExecutorOptions {
|
|
3
|
+
/** Execution timeout per call in ms (default 30000) */
|
|
4
|
+
timeout?: number;
|
|
5
|
+
/** Deno executable path ā 'deno' | auto-detect */
|
|
6
|
+
denoPath?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class DenoExecutor implements Executor {
|
|
9
|
+
private denoPath;
|
|
10
|
+
private timeout;
|
|
11
|
+
private process;
|
|
12
|
+
private readline;
|
|
13
|
+
private ready;
|
|
14
|
+
private readyResolve;
|
|
15
|
+
private pendingExecution;
|
|
16
|
+
private initPromise;
|
|
17
|
+
constructor(options?: DenoExecutorOptions);
|
|
18
|
+
private init;
|
|
19
|
+
private _init;
|
|
20
|
+
private handleMessage;
|
|
21
|
+
private handleToolCall;
|
|
22
|
+
private send;
|
|
23
|
+
execute(code: string, fns: Record<string, (...args: unknown[]) => Promise<unknown>>): Promise<ExecuteResult>;
|
|
24
|
+
dispose(): void;
|
|
25
|
+
}
|
|
26
|
+
export declare function createDenoExecutor(options?: DenoExecutorOptions): DenoExecutor;
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { spawn, execSync } from 'node:child_process';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { wrapCode } from './wrap-code.js';
|
|
6
|
+
// āā Runtime detection āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
7
|
+
function detectDeno(requested) {
|
|
8
|
+
if (requested)
|
|
9
|
+
return requested;
|
|
10
|
+
// If we are already running on Deno, use the current executable path
|
|
11
|
+
if (typeof globalThis.Deno?.execPath === 'function') {
|
|
12
|
+
return globalThis.Deno.execPath();
|
|
13
|
+
}
|
|
14
|
+
// Try 'deno' command in PATH
|
|
15
|
+
try {
|
|
16
|
+
execSync('deno --version', { stdio: 'ignore' });
|
|
17
|
+
return 'deno';
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// not available
|
|
21
|
+
}
|
|
22
|
+
throw new Error('Deno executable not found. Install Deno or set DENO_PATH environment variable.');
|
|
23
|
+
}
|
|
24
|
+
// āā Resolve runner script path āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
25
|
+
function getScriptPaths() {
|
|
26
|
+
let dir;
|
|
27
|
+
try {
|
|
28
|
+
dir = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
dir = __dirname;
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
runner: join(dir, 'container-runner.mjs'),
|
|
35
|
+
worker: join(dir, 'container-worker.mjs'),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// āā DenoExecutor āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
39
|
+
export class DenoExecutor {
|
|
40
|
+
constructor(options = {}) {
|
|
41
|
+
this.process = null;
|
|
42
|
+
this.readline = null;
|
|
43
|
+
this.ready = false;
|
|
44
|
+
this.readyResolve = null;
|
|
45
|
+
this.pendingExecution = null;
|
|
46
|
+
this.initPromise = null;
|
|
47
|
+
this.timeout = options.timeout ?? 30000;
|
|
48
|
+
this.denoPath = detectDeno(options.denoPath);
|
|
49
|
+
this.init().catch(err => {
|
|
50
|
+
process.stderr.write(`[deno-executor] Immediate initialization failed: ${err.message}\n`);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async init() {
|
|
54
|
+
if (this.ready)
|
|
55
|
+
return;
|
|
56
|
+
if (this.initPromise)
|
|
57
|
+
return this.initPromise;
|
|
58
|
+
this.initPromise = this._init();
|
|
59
|
+
return this.initPromise;
|
|
60
|
+
}
|
|
61
|
+
async _init() {
|
|
62
|
+
const scripts = getScriptPaths();
|
|
63
|
+
// Start a long-lived Deno process with restricted permissions
|
|
64
|
+
const args = [
|
|
65
|
+
'run',
|
|
66
|
+
'--no-prompt',
|
|
67
|
+
'--no-config',
|
|
68
|
+
'--no-npm',
|
|
69
|
+
'--no-remote',
|
|
70
|
+
'--allow-read=' + scripts.runner + ',' + scripts.worker,
|
|
71
|
+
'--deny-net',
|
|
72
|
+
'--deny-write',
|
|
73
|
+
'--deny-run',
|
|
74
|
+
'--deny-env',
|
|
75
|
+
'--deny-sys',
|
|
76
|
+
'--deny-ffi',
|
|
77
|
+
];
|
|
78
|
+
args.push(scripts.runner);
|
|
79
|
+
this.process = spawn(this.denoPath, args, {
|
|
80
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
81
|
+
});
|
|
82
|
+
this.process.stderr?.on('data', (data) => {
|
|
83
|
+
process.stderr.write(`[deno] ${data.toString()}`);
|
|
84
|
+
});
|
|
85
|
+
this.process.on('exit', (code) => {
|
|
86
|
+
this.ready = false;
|
|
87
|
+
if (this.pendingExecution) {
|
|
88
|
+
clearTimeout(this.pendingExecution.timeoutHandle);
|
|
89
|
+
this.pendingExecution.reject(new Error(`Deno process exited unexpectedly with code ${code}`));
|
|
90
|
+
this.pendingExecution = null;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
this.readline = createInterface({
|
|
94
|
+
input: this.process.stdout,
|
|
95
|
+
terminal: false,
|
|
96
|
+
});
|
|
97
|
+
this.readline.on('line', (line) => this.handleMessage(line));
|
|
98
|
+
await new Promise((resolve, reject) => {
|
|
99
|
+
const timeout = setTimeout(() => {
|
|
100
|
+
reject(new Error('Deno process failed to become ready within 10s'));
|
|
101
|
+
}, 10000);
|
|
102
|
+
this.readyResolve = () => {
|
|
103
|
+
clearTimeout(timeout);
|
|
104
|
+
this.ready = true;
|
|
105
|
+
this.readyResolve = null;
|
|
106
|
+
resolve();
|
|
107
|
+
};
|
|
108
|
+
this.process.on('error', (err) => {
|
|
109
|
+
clearTimeout(timeout);
|
|
110
|
+
this.readyResolve = null;
|
|
111
|
+
reject(new Error(`Failed to start Deno: ${err.message}`));
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
handleMessage(line) {
|
|
116
|
+
if (!line.trim())
|
|
117
|
+
return;
|
|
118
|
+
let msg;
|
|
119
|
+
try {
|
|
120
|
+
msg = JSON.parse(line);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
process.stderr.write(`[deno-executor] bad JSON from deno: ${line}\n`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
switch (msg.type) {
|
|
127
|
+
case 'tool-call':
|
|
128
|
+
this.handleToolCall(msg);
|
|
129
|
+
break;
|
|
130
|
+
case 'result':
|
|
131
|
+
if (this.pendingExecution && this.pendingExecution.id === msg.id) {
|
|
132
|
+
clearTimeout(this.pendingExecution.timeoutHandle);
|
|
133
|
+
this.pendingExecution.resolve({
|
|
134
|
+
result: msg.result,
|
|
135
|
+
logs: msg.logs,
|
|
136
|
+
});
|
|
137
|
+
this.pendingExecution = null;
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
case 'error':
|
|
141
|
+
if (this.pendingExecution && this.pendingExecution.id === msg.id) {
|
|
142
|
+
clearTimeout(this.pendingExecution.timeoutHandle);
|
|
143
|
+
this.pendingExecution.resolve({
|
|
144
|
+
result: undefined,
|
|
145
|
+
error: msg.error,
|
|
146
|
+
logs: msg.logs,
|
|
147
|
+
});
|
|
148
|
+
this.pendingExecution = null;
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
case 'ready':
|
|
152
|
+
if (this.readyResolve) {
|
|
153
|
+
this.readyResolve();
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async handleToolCall(msg) {
|
|
159
|
+
if (!this.pendingExecution) {
|
|
160
|
+
this.send({ type: 'tool-error', id: msg.id, error: 'No active execution context' });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const fns = this.pendingExecution.fns;
|
|
164
|
+
const fn = fns[msg.name];
|
|
165
|
+
if (!fn) {
|
|
166
|
+
this.send({
|
|
167
|
+
type: 'tool-error',
|
|
168
|
+
id: msg.id,
|
|
169
|
+
error: `Tool '${msg.name}' not found. Available tools: ${Object.keys(fns).join(', ')}`,
|
|
170
|
+
});
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const args = msg.args;
|
|
175
|
+
const result = await fn(...(Array.isArray(args) ? args : [args]));
|
|
176
|
+
this.send({ type: 'tool-result', id: msg.id, result });
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
this.send({
|
|
180
|
+
type: 'tool-error',
|
|
181
|
+
id: msg.id,
|
|
182
|
+
error: err instanceof Error ? err.message : String(err),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
send(msg) {
|
|
187
|
+
if (!this.process?.stdin?.writable) {
|
|
188
|
+
throw new Error('Deno stdin is not writable');
|
|
189
|
+
}
|
|
190
|
+
this.process.stdin.write(JSON.stringify(msg) + '\n');
|
|
191
|
+
}
|
|
192
|
+
async execute(code, fns) {
|
|
193
|
+
await this.init();
|
|
194
|
+
if (this.pendingExecution) {
|
|
195
|
+
return {
|
|
196
|
+
result: undefined,
|
|
197
|
+
error: 'Another execution is already in progress',
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const id = `exec-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
201
|
+
const wrappedCode = wrapCode(code);
|
|
202
|
+
return new Promise((resolve, reject) => {
|
|
203
|
+
const timeoutHandle = setTimeout(() => {
|
|
204
|
+
if (this.pendingExecution?.id === id) {
|
|
205
|
+
this.pendingExecution = null;
|
|
206
|
+
resolve({
|
|
207
|
+
result: undefined,
|
|
208
|
+
error: `Code execution timeout after ${this.timeout}ms`,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}, this.timeout);
|
|
212
|
+
this.pendingExecution = { id, resolve, reject, fns, timeoutHandle };
|
|
213
|
+
this.send({ type: 'execute', id, code: wrappedCode });
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
dispose() {
|
|
217
|
+
if (this.pendingExecution) {
|
|
218
|
+
clearTimeout(this.pendingExecution.timeoutHandle);
|
|
219
|
+
this.pendingExecution.reject(new Error('Executor disposed'));
|
|
220
|
+
this.pendingExecution = null;
|
|
221
|
+
}
|
|
222
|
+
if (this.process?.stdin?.writable) {
|
|
223
|
+
try {
|
|
224
|
+
this.send({ type: 'shutdown' });
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// ignore
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (this.readline) {
|
|
231
|
+
this.readline.close();
|
|
232
|
+
this.readline = null;
|
|
233
|
+
}
|
|
234
|
+
if (this.process) {
|
|
235
|
+
this.process.kill('SIGTERM');
|
|
236
|
+
const proc = this.process;
|
|
237
|
+
setTimeout(() => {
|
|
238
|
+
try {
|
|
239
|
+
proc.kill('SIGKILL');
|
|
240
|
+
}
|
|
241
|
+
catch { /* already dead */ }
|
|
242
|
+
}, 2000);
|
|
243
|
+
this.process = null;
|
|
244
|
+
}
|
|
245
|
+
this.ready = false;
|
|
246
|
+
this.initPromise = null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
export function createDenoExecutor(options) {
|
|
250
|
+
return new DenoExecutor(options);
|
|
251
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
|
2
|
+
import { getRuntimeName } from '../utils/env.js';
|
|
2
3
|
export function createExecutorTestSuite(name, createExecutor, options) {
|
|
3
4
|
const skipSet = new Set(options?.skipTests ?? []);
|
|
4
5
|
/** Use it.skip for tests in the skip list, it otherwise */
|
|
@@ -11,6 +12,7 @@ export function createExecutorTestSuite(name, createExecutor, options) {
|
|
|
11
12
|
}
|
|
12
13
|
};
|
|
13
14
|
describe(`Executor: ${name}`, () => {
|
|
15
|
+
console.log(`[Test] Runtime: ${getRuntimeName()}`);
|
|
14
16
|
let executor;
|
|
15
17
|
beforeAll(async () => {
|
|
16
18
|
executor = createExecutor();
|
|
@@ -335,7 +337,7 @@ export function createExecutorTestSuite(name, createExecutor, options) {
|
|
|
335
337
|
});
|
|
336
338
|
describe('Isolation & Safety', () => {
|
|
337
339
|
testOrSkip('should not allow access to require', async () => {
|
|
338
|
-
const result = await executor.execute('return require("fs");', {});
|
|
340
|
+
const result = await executor.execute('return require("node:fs");', {});
|
|
339
341
|
expect(result.error).toBeDefined();
|
|
340
342
|
});
|
|
341
343
|
testOrSkip('should not allow process access', async () => {
|
|
@@ -361,7 +363,7 @@ export function createExecutorTestSuite(name, createExecutor, options) {
|
|
|
361
363
|
return 'network allowed via fetch';
|
|
362
364
|
}
|
|
363
365
|
if (typeof require === 'function') {
|
|
364
|
-
const http = require('http');
|
|
366
|
+
const http = require('node:http');
|
|
365
367
|
await new Promise((resolve, reject) => {
|
|
366
368
|
http.get('http://example.com', resolve).on('error', reject);
|
|
367
369
|
});
|
|
@@ -389,7 +391,7 @@ export function createExecutorTestSuite(name, createExecutor, options) {
|
|
|
389
391
|
if (typeof require === 'function') {
|
|
390
392
|
// Try net.Socket (TCP)
|
|
391
393
|
try {
|
|
392
|
-
const net = require('net');
|
|
394
|
+
const net = require('node:net');
|
|
393
395
|
await new Promise((resolve, reject) => {
|
|
394
396
|
const sock = new net.Socket();
|
|
395
397
|
sock.setTimeout(2000);
|
|
@@ -402,7 +404,7 @@ export function createExecutorTestSuite(name, createExecutor, options) {
|
|
|
402
404
|
|
|
403
405
|
// Try dgram (UDP)
|
|
404
406
|
try {
|
|
405
|
-
const dgram = require('dgram');
|
|
407
|
+
const dgram = require('node:dgram');
|
|
406
408
|
const sock = dgram.createSocket('udp4');
|
|
407
409
|
await new Promise((resolve, reject) => {
|
|
408
410
|
sock.on('error', reject);
|
|
@@ -416,7 +418,7 @@ export function createExecutorTestSuite(name, createExecutor, options) {
|
|
|
416
418
|
|
|
417
419
|
// Try tls.connect
|
|
418
420
|
try {
|
|
419
|
-
const tls = require('tls');
|
|
421
|
+
const tls = require('node:tls');
|
|
420
422
|
await new Promise((resolve, reject) => {
|
|
421
423
|
const sock = tls.connect(443, '1.1.1.1', {}, resolve);
|
|
422
424
|
sock.on('error', reject);
|
|
@@ -426,7 +428,7 @@ export function createExecutorTestSuite(name, createExecutor, options) {
|
|
|
426
428
|
|
|
427
429
|
// Try dns.resolve
|
|
428
430
|
try {
|
|
429
|
-
const dns = require('dns');
|
|
431
|
+
const dns = require('node:dns');
|
|
430
432
|
await new Promise((resolve, reject) => {
|
|
431
433
|
dns.resolve('example.com', (err, addresses) => {
|
|
432
434
|
err ? reject(err) : resolve(addresses);
|
|
@@ -529,7 +531,7 @@ export function createExecutorTestSuite(name, createExecutor, options) {
|
|
|
529
531
|
testOrSkip('should block dynamic import', async () => {
|
|
530
532
|
const result = await executor.execute(`
|
|
531
533
|
try {
|
|
532
|
-
const m = await import('fs');
|
|
534
|
+
const m = await import('node:fs');
|
|
533
535
|
return 'import allowed';
|
|
534
536
|
} catch (e) {
|
|
535
537
|
return 'blocked';
|
package/dist/mcp/config.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Config Loader - Load MCP server configurations from files
|
|
3
3
|
* Supports VS Code's mcp.json format and other config files
|
|
4
4
|
*/
|
|
5
|
-
import * as fs from "fs";
|
|
6
|
-
import * as path from "path";
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as path from "node:path";
|
|
7
7
|
/**
|
|
8
8
|
* Load MCP server configurations from VS Code's mcp.json file
|
|
9
9
|
* Default location: ~/.config/Code/User/mcp.json (Linux/Mac) or
|
|
@@ -26,6 +26,7 @@ import { createCodeTool } from '@cloudflare/codemode/ai';
|
|
|
26
26
|
import { z } from 'zod';
|
|
27
27
|
import { adaptAISDKToolToMCP } from './mcp-adapter.js';
|
|
28
28
|
import { jsonSchemaToZod } from './server.js';
|
|
29
|
+
import { getRuntimeName } from '../utils/env.js';
|
|
29
30
|
// āā Helpers āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
30
31
|
/**
|
|
31
32
|
* Create a mock upstream MCP server with test tools and connect an MCP client
|
|
@@ -141,6 +142,7 @@ export function createE2EBridgeTestSuite(executorName, createExecutor, options)
|
|
|
141
142
|
}
|
|
142
143
|
};
|
|
143
144
|
describe(`E2E Bridge Pipeline [${executorName}]`, () => {
|
|
145
|
+
console.log(`[Test] Runtime: ${getRuntimeName()}`);
|
|
144
146
|
let upstreamState;
|
|
145
147
|
let bridgeState;
|
|
146
148
|
let client;
|
|
@@ -378,10 +380,10 @@ export function createE2EBridgeTestSuite(executorName, createExecutor, options)
|
|
|
378
380
|
testOrSkip('should not allow require access', async () => {
|
|
379
381
|
const response = await client.callTool({
|
|
380
382
|
name: 'eval',
|
|
381
|
-
arguments: { code: 'async () => { return require("fs"); }' },
|
|
383
|
+
arguments: { code: 'async () => { return require("node:fs"); }' },
|
|
382
384
|
});
|
|
383
385
|
const text = response.content?.[0]?.text || '';
|
|
384
|
-
expect(text.toLowerCase()).toMatch(/error|not defined|not allowed/);
|
|
386
|
+
expect(text.toLowerCase()).toMatch(/error|not defined|not allowed|requires .* access|could not be cloned/);
|
|
385
387
|
});
|
|
386
388
|
testOrSkip('should not allow process access', async () => {
|
|
387
389
|
const response = await client.callTool({
|
|
@@ -389,7 +391,7 @@ export function createE2EBridgeTestSuite(executorName, createExecutor, options)
|
|
|
389
391
|
arguments: { code: 'async () => { return process.env; }' },
|
|
390
392
|
});
|
|
391
393
|
const text = response.content?.[0]?.text || '';
|
|
392
|
-
expect(text.toLowerCase()).toMatch(/error|not defined|not allowed/);
|
|
394
|
+
expect(text.toLowerCase()).toMatch(/error|not defined|not allowed|requires .* access|could not be cloned/);
|
|
393
395
|
});
|
|
394
396
|
testOrSkip('should isolate state between executions', async () => {
|
|
395
397
|
await callCodemode(client, 'async () => { globalThis.__test = 123; return "set"; }');
|
package/dist/mcp/executor.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Selects the best available executor (isolated-vm ā container ā vm2)
|
|
4
4
|
*/
|
|
5
5
|
import type { Executor } from "@cloudflare/codemode";
|
|
6
|
-
export type ExecutorType = 'isolated-vm' | 'container' | 'vm2';
|
|
6
|
+
export type ExecutorType = 'isolated-vm' | 'container' | 'deno' | 'vm2';
|
|
7
7
|
/**
|
|
8
8
|
* Metadata about the executor that was created.
|
|
9
9
|
*/
|
|
@@ -19,13 +19,14 @@ export interface ExecutorInfo {
|
|
|
19
19
|
* Factory function to create an Executor instance.
|
|
20
20
|
*
|
|
21
21
|
* Selection logic:
|
|
22
|
+
* - If explicitType is provided, that executor is used (throws if unavailable).
|
|
22
23
|
* - If EXECUTOR_TYPE is set, that executor is used (throws if unavailable).
|
|
23
24
|
* - Otherwise, executors are tried in preference order (isolated-vm ā
|
|
24
25
|
* container ā vm2) and the first available one is selected.
|
|
25
26
|
*
|
|
26
27
|
* Returns both the executor and metadata about the selection.
|
|
27
28
|
*/
|
|
28
|
-
export declare function createExecutor(timeout?: number): Promise<{
|
|
29
|
+
export declare function createExecutor(timeout?: number, explicitType?: ExecutorType): Promise<{
|
|
29
30
|
executor: Executor;
|
|
30
31
|
info: ExecutorInfo;
|
|
31
32
|
}>;
|
package/dist/mcp/executor.js
CHANGED
|
@@ -3,44 +3,90 @@
|
|
|
3
3
|
* Selects the best available executor (isolated-vm ā container ā vm2)
|
|
4
4
|
*/
|
|
5
5
|
import { execFileSync } from "node:child_process";
|
|
6
|
-
import { logInfo } from "../utils/logger.js";
|
|
6
|
+
import { logInfo, logDebug } from "../utils/logger.js";
|
|
7
|
+
import { isNode, isDeno, isBun, getNodeMajorVersion } from "../utils/env.js";
|
|
7
8
|
// āā Availability checks (cached) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
8
9
|
let _isolatedVmAvailable = null;
|
|
10
|
+
async function isDenoAvailable() {
|
|
11
|
+
// Only allow Deno executor if running on Deno
|
|
12
|
+
return isDeno();
|
|
13
|
+
}
|
|
9
14
|
async function isIsolatedVmAvailable() {
|
|
10
15
|
if (_isolatedVmAvailable !== null)
|
|
11
16
|
return _isolatedVmAvailable;
|
|
17
|
+
// isolated-vm is a native module specifically built for Node.js.
|
|
18
|
+
// While Bun and Deno provide compatibility layers, they often fail or
|
|
19
|
+
// exhibit unstable behavior with native Node modules like isolated-vm.
|
|
20
|
+
// Additionally, isolated-vm only supports LTS (even-numbered) Node.js versions.
|
|
21
|
+
const majorVersion = getNodeMajorVersion();
|
|
22
|
+
const isEvenVersion = majorVersion > 0 && majorVersion % 2 === 0;
|
|
23
|
+
if (!isNode() || !isEvenVersion) {
|
|
24
|
+
logDebug('isolated-vm is only supported on LTS (even-numbered) versions of native Node.js (not Bun, Deno, or odd-numbered Node versions)', { component: 'Executor' });
|
|
25
|
+
_isolatedVmAvailable = false;
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
logDebug('Checking isolated-vm availability...', { component: 'Executor' });
|
|
12
29
|
try {
|
|
13
|
-
//
|
|
14
|
-
|
|
30
|
+
// We run the check in a separate process because isolated-vm can cause
|
|
31
|
+
// segmentation faults if native dependencies or environment are incompatible.
|
|
32
|
+
// Running it here ensures a crash doesn't take down the main bridge process.
|
|
33
|
+
const checkScript = `
|
|
34
|
+
import ivm from 'isolated-vm';
|
|
35
|
+
const isolate = new ivm.Isolate({ memoryLimit: 8 });
|
|
36
|
+
isolate.dispose();
|
|
37
|
+
process.exit(0);
|
|
38
|
+
`;
|
|
39
|
+
const { execSync } = await import('node:child_process');
|
|
40
|
+
execSync(`node -e "${checkScript.replace(/"/g, '\\"').replace(/\n/g, '')}"`, { stdio: 'ignore', timeout: 5000 });
|
|
41
|
+
logDebug('isolated-vm is available and functional (verified via subprocess)', { component: 'Executor' });
|
|
15
42
|
_isolatedVmAvailable = true;
|
|
16
43
|
}
|
|
17
|
-
catch {
|
|
44
|
+
catch (err) {
|
|
45
|
+
logDebug(`isolated-vm check failed or crashed: ${err instanceof Error ? err.message : String(err)}`, { component: 'Executor' });
|
|
18
46
|
_isolatedVmAvailable = false;
|
|
19
47
|
}
|
|
20
48
|
return _isolatedVmAvailable;
|
|
21
49
|
}
|
|
22
50
|
let _containerRuntimeAvailable = null;
|
|
23
|
-
function isContainerRuntimeAvailable() {
|
|
51
|
+
async function isContainerRuntimeAvailable() {
|
|
24
52
|
if (_containerRuntimeAvailable !== null)
|
|
25
53
|
return _containerRuntimeAvailable;
|
|
54
|
+
logDebug('Checking container runtime availability...', { component: 'Executor' });
|
|
55
|
+
// Check for docker or podman
|
|
26
56
|
for (const cmd of ['docker', 'podman']) {
|
|
27
57
|
try {
|
|
28
|
-
|
|
58
|
+
logDebug(`Testing container runtime: ${cmd}`, { component: 'Executor' });
|
|
59
|
+
// Use 'ps' instead of '--version' because 'ps' requires the
|
|
60
|
+
// runtime daemon/service to be actually running and responsive.
|
|
61
|
+
// execFileSync is okay here as it's a one-time check during startup/first-use.
|
|
62
|
+
execFileSync(cmd, ['ps'], { stdio: 'ignore', timeout: 3000 });
|
|
63
|
+
logDebug(`Container runtime "${cmd}" is available and responsive`, { component: 'Executor' });
|
|
29
64
|
_containerRuntimeAvailable = true;
|
|
30
65
|
return true;
|
|
31
66
|
}
|
|
32
|
-
catch {
|
|
33
|
-
|
|
67
|
+
catch (err) {
|
|
68
|
+
logDebug(`Container runtime "${cmd}" check failed: ${err instanceof Error ? err.message : String(err)}`, { component: 'Executor' });
|
|
69
|
+
// not available or not running
|
|
34
70
|
}
|
|
35
71
|
}
|
|
72
|
+
logDebug('No responsive container runtime found', { component: 'Executor' });
|
|
36
73
|
_containerRuntimeAvailable = false;
|
|
37
74
|
return false;
|
|
38
75
|
}
|
|
39
76
|
// āā Executor registry (sorted by preference, lowest first) āāāāāāāāā
|
|
40
77
|
const executorRegistry = [
|
|
41
78
|
{
|
|
42
|
-
type: '
|
|
79
|
+
type: 'deno',
|
|
43
80
|
preference: 0,
|
|
81
|
+
isAvailable: isDenoAvailable,
|
|
82
|
+
async create(timeout) {
|
|
83
|
+
const { createDenoExecutor } = await import('../executor/deno-executor.js');
|
|
84
|
+
return createDenoExecutor({ timeout });
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
type: 'isolated-vm',
|
|
89
|
+
preference: 1,
|
|
44
90
|
isAvailable: isIsolatedVmAvailable,
|
|
45
91
|
async create(timeout) {
|
|
46
92
|
const { createIsolatedVmExecutor } = await import('../executor/isolated-vm-executor.js');
|
|
@@ -49,8 +95,8 @@ const executorRegistry = [
|
|
|
49
95
|
},
|
|
50
96
|
{
|
|
51
97
|
type: 'container',
|
|
52
|
-
preference:
|
|
53
|
-
isAvailable:
|
|
98
|
+
preference: 2,
|
|
99
|
+
isAvailable: isContainerRuntimeAvailable,
|
|
54
100
|
async create(timeout) {
|
|
55
101
|
const { createContainerExecutor } = await import('../executor/container-executor.js');
|
|
56
102
|
return createContainerExecutor({ timeout });
|
|
@@ -58,8 +104,12 @@ const executorRegistry = [
|
|
|
58
104
|
},
|
|
59
105
|
{
|
|
60
106
|
type: 'vm2',
|
|
61
|
-
preference:
|
|
107
|
+
preference: 3,
|
|
62
108
|
isAvailable: async () => {
|
|
109
|
+
// vm2 is fundamentally broken on Bun (prototype freezing issues)
|
|
110
|
+
// and Node.js built-in 'node:vm' is not safe for untrusted code.
|
|
111
|
+
if (isBun())
|
|
112
|
+
return false;
|
|
63
113
|
try {
|
|
64
114
|
await import('vm2');
|
|
65
115
|
return true;
|
|
@@ -79,26 +129,27 @@ const executorRegistry = [
|
|
|
79
129
|
* Factory function to create an Executor instance.
|
|
80
130
|
*
|
|
81
131
|
* Selection logic:
|
|
132
|
+
* - If explicitType is provided, that executor is used (throws if unavailable).
|
|
82
133
|
* - If EXECUTOR_TYPE is set, that executor is used (throws if unavailable).
|
|
83
134
|
* - Otherwise, executors are tried in preference order (isolated-vm ā
|
|
84
135
|
* container ā vm2) and the first available one is selected.
|
|
85
136
|
*
|
|
86
137
|
* Returns both the executor and metadata about the selection.
|
|
87
138
|
*/
|
|
88
|
-
export async function createExecutor(timeout = 30000) {
|
|
89
|
-
const requested = process.env.EXECUTOR_TYPE?.toLowerCase();
|
|
139
|
+
export async function createExecutor(timeout = 30000, explicitType) {
|
|
140
|
+
const requested = (explicitType || process.env.EXECUTOR_TYPE?.toLowerCase());
|
|
90
141
|
// Explicit selection ā must succeed or throw
|
|
91
142
|
if (requested) {
|
|
92
143
|
const entry = executorRegistry.find(e => e.type === requested);
|
|
93
144
|
if (!entry) {
|
|
94
145
|
const known = executorRegistry.map(e => e.type).join(', ');
|
|
95
|
-
throw new Error(`Unknown
|
|
146
|
+
throw new Error(`Unknown executor type "${requested}". Valid types: ${known}`);
|
|
96
147
|
}
|
|
97
148
|
const available = await entry.isAvailable();
|
|
98
149
|
if (!available) {
|
|
99
|
-
throw new Error(`
|
|
150
|
+
throw new Error(`Executor type ${requested} requested but it is not available in this environment.`);
|
|
100
151
|
}
|
|
101
|
-
logInfo(`Using ${entry.type} executor (EXECUTOR_TYPE=${requested})`, { component: 'Executor' });
|
|
152
|
+
logInfo(`Using ${entry.type} executor (${explicitType ? 'explicit option' : `EXECUTOR_TYPE=${requested}`})`, { component: 'Executor' });
|
|
102
153
|
return {
|
|
103
154
|
executor: await entry.create(timeout),
|
|
104
155
|
info: { type: entry.type, reason: 'explicit', timeout },
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* to our loopback server with the authorization code, which we capture and use
|
|
7
7
|
* to complete the authorization flow.
|
|
8
8
|
*/
|
|
9
|
-
import { createServer } from 'http';
|
|
10
|
-
import { URL } from 'url';
|
|
9
|
+
import { createServer } from 'node:http';
|
|
10
|
+
import { URL } from 'node:url';
|
|
11
11
|
/**
|
|
12
12
|
* Loopback HTTP server that listens for OAuth2 redirect callbacks
|
|
13
13
|
*/
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -15,11 +15,12 @@
|
|
|
15
15
|
* 6. Exposes the "codemode" tool via MCP protocol downstream
|
|
16
16
|
*/
|
|
17
17
|
import { z } from "zod";
|
|
18
|
+
import { type ExecutorType } from "./executor.js";
|
|
18
19
|
import { type MCPServerConfig } from "./mcp-client.js";
|
|
19
|
-
export type { MCPServerConfig };
|
|
20
|
+
export type { MCPServerConfig, ExecutorType };
|
|
20
21
|
/**
|
|
21
22
|
* Convert JSON Schema to Zod schema
|
|
22
23
|
* MCP tools use JSON Schema, but createCodeTool expects Zod schemas
|
|
23
24
|
*/
|
|
24
25
|
export declare function jsonSchemaToZod(schema: any): z.ZodType<any>;
|
|
25
|
-
export declare function startCodeModeBridgeServer(serverConfigs: MCPServerConfig[]): Promise<void>;
|
|
26
|
+
export declare function startCodeModeBridgeServer(serverConfigs: MCPServerConfig[], executorType?: ExecutorType): Promise<void>;
|
package/dist/mcp/server.js
CHANGED
|
@@ -219,7 +219,7 @@ function convertMCPToolToDescriptor(toolDef, client, toolName, serverName) {
|
|
|
219
219
|
},
|
|
220
220
|
};
|
|
221
221
|
}
|
|
222
|
-
export async function startCodeModeBridgeServer(serverConfigs) {
|
|
222
|
+
export async function startCodeModeBridgeServer(serverConfigs, executorType) {
|
|
223
223
|
// Enable buffering of stderr output from stdio tools during startup
|
|
224
224
|
enableStderrBuffering();
|
|
225
225
|
const mcp = new McpServer({
|
|
@@ -276,7 +276,7 @@ export async function startCodeModeBridgeServer(serverConfigs) {
|
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
278
|
// Create the executor using the codemode SDK pattern
|
|
279
|
-
const { executor, info: executorInfo } = await createExecutor(30000); // 30 second timeout
|
|
279
|
+
const { executor, info: executorInfo } = await createExecutor(30000, executorType); // 30 second timeout
|
|
280
280
|
// Create the codemode tool using the codemode SDK
|
|
281
281
|
// Pass ToolDescriptor format (with Zod schemas and execute functions)
|
|
282
282
|
logInfo(`Creating codemode tool with ${totalToolCount} tools from ${serverConfigs.length} server(s)`, { component: 'Bridge' });
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* Persists OAuth tokens and client information to disk for reuse across sessions.
|
|
5
5
|
* Stored in ~/.config/codemode-bridge/mcp-tokens.json
|
|
6
6
|
*/
|
|
7
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
8
|
-
import { join } from 'path';
|
|
9
|
-
import { homedir } from 'os';
|
|
7
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
10
|
/**
|
|
11
11
|
* Manages OAuth token storage for MCP server connections
|
|
12
12
|
*/
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment detection utilities
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Returns true if the current runtime is Node.js
|
|
6
|
+
*/
|
|
7
|
+
export declare function isNode(): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Returns the Node.js major version, or 0 if not running on Node.js
|
|
10
|
+
*/
|
|
11
|
+
export declare function getNodeMajorVersion(): number;
|
|
12
|
+
/**
|
|
13
|
+
* Returns true if the current runtime is Bun
|
|
14
|
+
*/
|
|
15
|
+
export declare function isBun(): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Returns true if the current runtime is Deno
|
|
18
|
+
*/
|
|
19
|
+
export declare function isDeno(): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Returns the name of the current runtime (Node.js, Bun, or Deno)
|
|
22
|
+
*/
|
|
23
|
+
export declare function getRuntimeName(): string;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment detection utilities
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Returns true if the current runtime is Node.js
|
|
6
|
+
*/
|
|
7
|
+
export function isNode() {
|
|
8
|
+
return (typeof process !== 'undefined' &&
|
|
9
|
+
!!process.versions?.node &&
|
|
10
|
+
!isBun() &&
|
|
11
|
+
!isDeno());
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Returns the Node.js major version, or 0 if not running on Node.js
|
|
15
|
+
*/
|
|
16
|
+
export function getNodeMajorVersion() {
|
|
17
|
+
if (typeof process === 'undefined' || !process.versions?.node) {
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
return parseInt(process.versions.node.split('.')[0], 10);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Returns true if the current runtime is Bun
|
|
24
|
+
*/
|
|
25
|
+
export function isBun() {
|
|
26
|
+
return (typeof process !== 'undefined' &&
|
|
27
|
+
!!process.versions?.bun);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Returns true if the current runtime is Deno
|
|
31
|
+
*/
|
|
32
|
+
export function isDeno() {
|
|
33
|
+
return ((typeof globalThis !== 'undefined' && !!globalThis.Deno) ||
|
|
34
|
+
(typeof process !== 'undefined' && !!process.versions?.deno));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Returns the name of the current runtime (Node.js, Bun, or Deno)
|
|
38
|
+
*/
|
|
39
|
+
export function getRuntimeName() {
|
|
40
|
+
if (isBun())
|
|
41
|
+
return "Bun";
|
|
42
|
+
if (isDeno())
|
|
43
|
+
return "Deno";
|
|
44
|
+
if (isNode())
|
|
45
|
+
return "Node.js";
|
|
46
|
+
return "Unknown";
|
|
47
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ruifung/codemode-bridge",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "MCP bridge that connects to upstream MCP servers and exposes tools via a single codemode tool for orchestration",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"prepare": "npm run build",
|
|
16
16
|
"dev": "tsx src/cli/index.ts",
|
|
17
17
|
"test": "vitest",
|
|
18
|
-
"test:e2e": "vitest run src/mcp/e2e-bridge-runner.test.ts"
|
|
18
|
+
"test:e2e": "vitest run src/mcp/e2e-bridge-runner.test.ts --reporter=verbose"
|
|
19
19
|
},
|
|
20
20
|
"keywords": [
|
|
21
21
|
"mcp",
|