@ruifung/codemode-bridge 1.0.8 → 1.0.9
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/index.js +14 -1
- package/dist/executor/container-executor.js +18 -11
- package/dist/executor/deno-executor.js +8 -6
- package/dist/executor/isolated-vm-executor.js +4 -2
- package/dist/executor/vm2-executor.js +1 -0
- package/dist/mcp/executor-status.d.ts +6 -0
- package/dist/mcp/executor-status.js +58 -0
- package/dist/mcp/mcp-client.js +14 -3
- package/dist/mcp/oauth-handler.js +4 -2
- package/dist/mcp/token-persistence.js +3 -1
- package/package.json +5 -2
package/dist/cli/index.js
CHANGED
|
@@ -19,14 +19,27 @@
|
|
|
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 { getExecutorStatus } from "../mcp/executor-status.js";
|
|
22
23
|
import * as fs from "node:fs";
|
|
23
24
|
const pkg = JSON.parse(fs.readFileSync(new URL("../../package.json", import.meta.url), "utf-8"));
|
|
24
25
|
const defaultConfigPath = getConfigFilePath();
|
|
25
26
|
const program = new Command();
|
|
27
|
+
// Add logic to get available executors for version output
|
|
28
|
+
let versionString = pkg.version;
|
|
29
|
+
if (process.argv.includes("-V") || process.argv.includes("--version")) {
|
|
30
|
+
const statuses = await getExecutorStatus();
|
|
31
|
+
const available = statuses
|
|
32
|
+
.filter((s) => s.isAvailable)
|
|
33
|
+
.map((s) => s.type)
|
|
34
|
+
.join(", ");
|
|
35
|
+
if (available) {
|
|
36
|
+
versionString = `${pkg.version} (executors: ${available})`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
26
39
|
program
|
|
27
40
|
.name("codemode-bridge")
|
|
28
41
|
.description("Code Mode Bridge CLI - Connects to multiple MCP servers and exposes them as a single tool")
|
|
29
|
-
.version(
|
|
42
|
+
.version(versionString);
|
|
30
43
|
// Main 'run' command
|
|
31
44
|
program
|
|
32
45
|
.command("run", { isDefault: true })
|
|
@@ -57,18 +57,25 @@ function getScriptPaths() {
|
|
|
57
57
|
}
|
|
58
58
|
// ── ContainerExecutor ───────────────────────────────────────────────
|
|
59
59
|
export class ContainerExecutor {
|
|
60
|
+
runtime;
|
|
61
|
+
image;
|
|
62
|
+
containerUser;
|
|
63
|
+
containerCommand;
|
|
64
|
+
timeout;
|
|
65
|
+
memoryLimit;
|
|
66
|
+
cpuLimit;
|
|
67
|
+
containerId = null;
|
|
68
|
+
process = null;
|
|
69
|
+
readline = null;
|
|
70
|
+
ready = false;
|
|
71
|
+
/** Resolved when the container sends { type: 'ready' } */
|
|
72
|
+
readyResolve = null;
|
|
73
|
+
/** Pending execution — only one at a time */
|
|
74
|
+
pendingExecution = null;
|
|
75
|
+
initPromise = null;
|
|
60
76
|
constructor(options = {}) {
|
|
61
|
-
this.containerId = null;
|
|
62
|
-
this.process = null;
|
|
63
|
-
this.readline = null;
|
|
64
|
-
this.ready = false;
|
|
65
|
-
/** Resolved when the container sends { type: 'ready' } */
|
|
66
|
-
this.readyResolve = null;
|
|
67
|
-
/** Pending execution — only one at a time */
|
|
68
|
-
this.pendingExecution = null;
|
|
69
|
-
this.initPromise = null;
|
|
70
77
|
this.timeout = options.timeout ?? 30000;
|
|
71
|
-
this.memoryLimit = options.memoryLimit ?? '
|
|
78
|
+
this.memoryLimit = options.memoryLimit ?? '512M';
|
|
72
79
|
this.cpuLimit = options.cpuLimit ?? 1.0;
|
|
73
80
|
this.runtime = detectRuntime(options.runtime);
|
|
74
81
|
// Platform-specific defaults
|
|
@@ -171,7 +178,7 @@ export class ContainerExecutor {
|
|
|
171
178
|
'--user', this.containerUser,
|
|
172
179
|
'--memory', this.memoryLimit,
|
|
173
180
|
`--cpus=${this.cpuLimit}`,
|
|
174
|
-
'--pids-limit=
|
|
181
|
+
'--pids-limit=128', // limit process spawning
|
|
175
182
|
'-v', `${scripts.runner}:/app/container-runner.mjs:ro`, // mount runner script
|
|
176
183
|
'-v', `${scripts.worker}:/app/container-worker.mjs:ro`, // mount worker script
|
|
177
184
|
'-w', '/app',
|
|
@@ -37,13 +37,15 @@ function getScriptPaths() {
|
|
|
37
37
|
}
|
|
38
38
|
// ── DenoExecutor ────────────────────────────────────────────────────
|
|
39
39
|
export class DenoExecutor {
|
|
40
|
+
denoPath;
|
|
41
|
+
timeout;
|
|
42
|
+
process = null;
|
|
43
|
+
readline = null;
|
|
44
|
+
ready = false;
|
|
45
|
+
readyResolve = null;
|
|
46
|
+
pendingExecution = null;
|
|
47
|
+
initPromise = null;
|
|
40
48
|
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
49
|
this.timeout = options.timeout ?? 30000;
|
|
48
50
|
this.denoPath = detectDeno(options.denoPath);
|
|
49
51
|
this.init().catch(err => {
|
|
@@ -55,9 +55,11 @@ function stringify(value) {
|
|
|
55
55
|
* - Explicit serialization boundaries
|
|
56
56
|
*/
|
|
57
57
|
export class IsolatedVmExecutor {
|
|
58
|
+
isolate;
|
|
59
|
+
context = null;
|
|
60
|
+
metrics = null;
|
|
61
|
+
options;
|
|
58
62
|
constructor(options = {}) {
|
|
59
|
-
this.context = null;
|
|
60
|
-
this.metrics = null;
|
|
61
63
|
this.options = {
|
|
62
64
|
memoryLimit: options.memoryLimit ?? 128,
|
|
63
65
|
inspector: options.inspector ?? false,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { isNode, isDeno, isBun, getNodeMajorVersion } from "../utils/env.js";
|
|
3
|
+
async function isDenoAvailable() {
|
|
4
|
+
return isDeno();
|
|
5
|
+
}
|
|
6
|
+
async function isIsolatedVmAvailable() {
|
|
7
|
+
const majorVersion = getNodeMajorVersion();
|
|
8
|
+
const isEvenVersion = majorVersion > 0 && majorVersion % 2 === 0;
|
|
9
|
+
if (!isNode() || !isEvenVersion) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const checkScript = `
|
|
14
|
+
import ivm from 'isolated-vm';
|
|
15
|
+
const isolate = new ivm.Isolate({ memoryLimit: 8 });
|
|
16
|
+
isolate.dispose();
|
|
17
|
+
process.exit(0);
|
|
18
|
+
`;
|
|
19
|
+
const { execSync } = await import('node:child_process');
|
|
20
|
+
execSync(`node -e "${checkScript.replace(/"/g, '\\"').replace(/\n/g, '')}"`, { stdio: 'ignore', timeout: 2000 });
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function isContainerRuntimeAvailable() {
|
|
28
|
+
for (const cmd of ['docker', 'podman']) {
|
|
29
|
+
try {
|
|
30
|
+
execFileSync(cmd, ['ps'], { stdio: 'ignore', timeout: 2000 });
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
// not available
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
async function isVM2Available() {
|
|
40
|
+
if (isBun())
|
|
41
|
+
return false;
|
|
42
|
+
try {
|
|
43
|
+
await import('vm2');
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function getExecutorStatus() {
|
|
51
|
+
const statuses = [
|
|
52
|
+
{ type: 'deno', isAvailable: await isDenoAvailable() },
|
|
53
|
+
{ type: 'isolated-vm', isAvailable: await isIsolatedVmAvailable() },
|
|
54
|
+
{ type: 'container', isAvailable: await isContainerRuntimeAvailable() },
|
|
55
|
+
{ type: 'vm2', isAvailable: await isVM2Available() },
|
|
56
|
+
];
|
|
57
|
+
return statuses;
|
|
58
|
+
}
|
package/dist/mcp/mcp-client.js
CHANGED
|
@@ -19,6 +19,15 @@ import { logDebug, logInfo } from "../utils/logger.js";
|
|
|
19
19
|
* Supports both pre-registered clients and dynamic client registration (RFC 7591)
|
|
20
20
|
*/
|
|
21
21
|
class SimpleOAuthProvider {
|
|
22
|
+
config;
|
|
23
|
+
serverUrl;
|
|
24
|
+
static DEFAULT_REDIRECT_URL = 'http://localhost:3000/oauth/callback';
|
|
25
|
+
_tokens;
|
|
26
|
+
_codeVerifier;
|
|
27
|
+
_clientInfo;
|
|
28
|
+
_discoveryState;
|
|
29
|
+
_callbackServer;
|
|
30
|
+
_finishAuthCallback;
|
|
22
31
|
constructor(config, serverUrl) {
|
|
23
32
|
this.config = config;
|
|
24
33
|
this.serverUrl = serverUrl;
|
|
@@ -196,15 +205,17 @@ class SimpleOAuthProvider {
|
|
|
196
205
|
return undefined;
|
|
197
206
|
}
|
|
198
207
|
}
|
|
199
|
-
SimpleOAuthProvider.DEFAULT_REDIRECT_URL = 'http://localhost:3000/oauth/callback';
|
|
200
208
|
/**
|
|
201
209
|
* MCP Client wrapper using official SDK
|
|
202
210
|
*/
|
|
203
211
|
export class MCPClient {
|
|
212
|
+
config;
|
|
213
|
+
client;
|
|
214
|
+
transport = null;
|
|
215
|
+
connected = false;
|
|
216
|
+
oauthProvider;
|
|
204
217
|
constructor(config) {
|
|
205
218
|
this.config = config;
|
|
206
|
-
this.transport = null;
|
|
207
|
-
this.connected = false;
|
|
208
219
|
this.client = new Client({
|
|
209
220
|
name: `codemode-bridge-client-${config.name}`,
|
|
210
221
|
version: "1.0.0",
|
|
@@ -12,9 +12,11 @@ import { URL } from 'node:url';
|
|
|
12
12
|
* Loopback HTTP server that listens for OAuth2 redirect callbacks
|
|
13
13
|
*/
|
|
14
14
|
export class OAuthCallbackServer {
|
|
15
|
+
server;
|
|
16
|
+
port = 0; // 0 means OS will assign an available port
|
|
17
|
+
host = 'localhost';
|
|
18
|
+
pendingAuthorization;
|
|
15
19
|
constructor(redirectUrl) {
|
|
16
|
-
this.port = 0; // 0 means OS will assign an available port
|
|
17
|
-
this.host = 'localhost';
|
|
18
20
|
// Parse redirect URL to extract host and port
|
|
19
21
|
if (redirectUrl) {
|
|
20
22
|
try {
|
|
@@ -11,8 +11,10 @@ import { homedir } from 'node:os';
|
|
|
11
11
|
* Manages OAuth token storage for MCP server connections
|
|
12
12
|
*/
|
|
13
13
|
export class TokenPersistence {
|
|
14
|
+
configDir;
|
|
15
|
+
tokenFile;
|
|
16
|
+
storage = {};
|
|
14
17
|
constructor() {
|
|
15
|
-
this.storage = {};
|
|
16
18
|
this.configDir = join(homedir(), '.config', 'codemode-bridge');
|
|
17
19
|
this.tokenFile = join(this.configDir, 'mcp-tokens.json');
|
|
18
20
|
this.loadStorage();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ruifung/codemode-bridge",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
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": {
|
|
@@ -14,8 +14,11 @@
|
|
|
14
14
|
"build": "tsc",
|
|
15
15
|
"prepare": "npm run build",
|
|
16
16
|
"dev": "tsx src/cli/index.ts",
|
|
17
|
+
"dev:bun": "bun run src/cli/index.ts",
|
|
18
|
+
"dev:deno": "deno run --allow-net --allow-read src/cli/index.ts",
|
|
17
19
|
"test": "vitest",
|
|
18
|
-
"test:
|
|
20
|
+
"test:bun": "bun test",
|
|
21
|
+
"test:deno": "deno run -A npm:vitest"
|
|
19
22
|
},
|
|
20
23
|
"keywords": [
|
|
21
24
|
"mcp",
|