@ruifung/codemode-bridge 1.0.5 → 1.0.7

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.
@@ -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
  */
@@ -15,7 +15,7 @@ import { MCPClient } from "../mcp/mcp-client.js";
15
15
  /**
16
16
  * Run the bridge server
17
17
  */
18
- export async function runServer(configPath, servers, debug) {
18
+ export async function runServer(configPath, servers, debug, executor) {
19
19
  try {
20
20
  // Initialize logger with debug mode if requested
21
21
  initializeLogger(debug);
@@ -53,7 +53,7 @@ export async function runServer(configPath, servers, debug) {
53
53
  }
54
54
  logInfo(`Starting bridge with ${serverConfigs.length} server(s)`, { component: 'CLI' });
55
55
  // Start the MCP bridge server
56
- await startCodeModeBridgeServer(serverConfigs);
56
+ await startCodeModeBridgeServer(serverConfigs, executor);
57
57
  // Flush buffered stderr output from stdio tools now that Bridge is fully running
58
58
  flushStderrBuffer();
59
59
  logInfo("Bridge is running!", { component: 'CLI' });
@@ -136,7 +136,7 @@ export function addServerCommand(name, options, configPath) {
136
136
  };
137
137
  if (options.type === "stdio") {
138
138
  if (!options.command) {
139
- console.error(chalk.red("✗") + ' Missing --command for stdio server');
139
+ console.error(chalk.red("✗") + " Missing command for stdio server");
140
140
  process.exit(1);
141
141
  }
142
142
  entry.command = options.command;
@@ -146,7 +146,7 @@ export function addServerCommand(name, options, configPath) {
146
146
  }
147
147
  else if (options.type === "http") {
148
148
  if (!options.url) {
149
- console.error(chalk.red("✗") + ' Missing --url for http server');
149
+ console.error(chalk.red("✗") + " Missing --url for http server");
150
150
  process.exit(1);
151
151
  }
152
152
  entry.url = options.url;
package/dist/cli/index.js CHANGED
@@ -18,20 +18,26 @@
18
18
  */
19
19
  import { Command } from "commander";
20
20
  import { runServer, listServersCommand, showServerCommand, addServerCommand, removeServerCommand, editServerCommand, configInfoCommand, authLoginCommand, authLogoutCommand, authListCommand, } from "./commands.js";
21
+ import { getConfigFilePath } from "./config-manager.js";
21
22
  import * as fs from "fs";
22
23
  const pkg = JSON.parse(fs.readFileSync(new URL("../../package.json", import.meta.url), "utf-8"));
24
+ const defaultConfigPath = getConfigFilePath();
23
25
  const program = new Command();
24
- program.name("codemode-bridge").description("Code Mode Bridge CLI").version(pkg.version);
26
+ program
27
+ .name("codemode-bridge")
28
+ .description("Code Mode Bridge CLI - Connects to multiple MCP servers and exposes them as a single tool")
29
+ .version(pkg.version);
25
30
  // Main 'run' command
26
31
  program
27
- .command("run")
32
+ .command("run", { isDefault: true })
28
33
  .description("Start the bridge MCP server (default command)")
29
- .option("-c, --config <path>", "Path to mcp.json configuration file (default: ~/.config/codemode-bridge/mcp.json)")
34
+ .option("-c, --config <path>", `Path to mcp.json configuration file (default: ${defaultConfigPath})`)
30
35
  .option("-s, --servers <names>", "Comma-separated list of servers to connect to")
31
36
  .option("-d, --debug", "Enable debug logging")
37
+ .option("-e, --executor <type>", "Executor type (isolated-vm, container, vm2)")
32
38
  .action(async (options) => {
33
39
  const servers = options.servers ? options.servers.split(",").map((s) => s.trim()) : undefined;
34
- await runServer(options.config, servers, options.debug);
40
+ await runServer(options.config, servers, options.debug, options.executor);
35
41
  });
36
42
  // Config command group
37
43
  const config = program.command("config").description("Manage bridge configuration").enablePositionalOptions();
@@ -43,20 +49,27 @@ config
43
49
  listServersCommand(options.config);
44
50
  });
45
51
  config
46
- .command("show <name>")
52
+ .command("show <server-name>")
47
53
  .description("Show a server configuration")
48
54
  .option("-c, --config <path>", "Path to mcp.json configuration file")
49
55
  .action((name, options) => {
50
56
  showServerCommand(name, options.config);
51
57
  });
52
58
  config
53
- .command("add <name> [commandAndArgs...]")
59
+ .command("add <server-name> [commandAndArgs...]")
54
60
  .description("Add a new server configuration (use -- before commands with flags, e.g. -- npx -y @some/pkg)")
55
61
  .passThroughOptions()
56
62
  .requiredOption("-t, --type <type>", "Server type (stdio or http)")
57
63
  .option("--url <url>", "Server URL (required for http servers)")
58
64
  .option("--env <env...>", 'Environment variables as KEY=VALUE pairs')
59
65
  .option("-c, --config <path>", "Path to mcp.json configuration file")
66
+ .addHelpText("after", `
67
+ Examples:
68
+ $ codemode-bridge config add my-server --type stdio node /path/to/server.js
69
+ $ codemode-bridge config add remote-server --type http --url https://api.example.com/mcp
70
+ $ codemode-bridge config add secure-server --type stdio --env API_KEY=secret python server.py
71
+ $ codemode-bridge config add npx-server --type stdio -- npx -y @modelcontextprotocol/server-everything
72
+ `)
60
73
  .action((name, commandAndArgs, options) => {
61
74
  let command;
62
75
  let args;
@@ -85,19 +98,25 @@ config
85
98
  }, options.config);
86
99
  });
87
100
  config
88
- .command("remove <name>")
101
+ .command("remove <server-name>")
89
102
  .description("Remove a server configuration")
90
103
  .option("-c, --config <path>", "Path to mcp.json configuration file")
91
104
  .action((name, options) => {
92
105
  removeServerCommand(name, options.config);
93
106
  });
94
107
  config
95
- .command("edit <name> [commandAndArgs...]")
108
+ .command("edit <server-name> [commandAndArgs...]")
96
109
  .description("Edit a server configuration")
97
110
  .option("-t, --type <type>", "Server type (stdio or http)")
98
111
  .option("--url <url>", "Server URL (for http servers)")
99
112
  .option("--env <env...>", 'Environment variables as KEY=VALUE pairs')
100
113
  .option("-c, --config <path>", "Path to mcp.json configuration file")
114
+ .addHelpText("after", `
115
+ Examples:
116
+ $ codemode-bridge config edit my-server node /new/path/to/server.js
117
+ $ codemode-bridge config edit remote-server --url https://new-api.example.com/mcp
118
+ $ codemode-bridge config edit secure-server --env API_KEY=new-secret
119
+ `)
101
120
  .action((name, commandAndArgs, options) => {
102
121
  let command;
103
122
  let args;
@@ -155,12 +174,4 @@ auth
155
174
  .action((serverName, options) => {
156
175
  authLogoutCommand(serverName, options.config);
157
176
  });
158
- // Default command: run if no command specified
159
- program.action(async () => {
160
- // If no command is specified, run the bridge
161
- const args = process.argv.slice(2);
162
- if (args.length === 0) {
163
- await runServer(undefined, undefined, false);
164
- }
165
- });
166
177
  program.parse();
@@ -71,6 +71,11 @@ export class ContainerExecutor {
71
71
  this.memoryLimit = options.memoryLimit ?? '256m';
72
72
  this.cpuLimit = options.cpuLimit ?? 1.0;
73
73
  this.runtime = detectRuntime(options.runtime);
74
+ // Start initialization immediately to create the container before the first execution.
75
+ // Errors are caught and logged, but will also be thrown when execute() awaits this.init().
76
+ this.init().catch(err => {
77
+ process.stderr.write(`[container-executor] Immediate initialization failed: ${err.message}\n`);
78
+ });
74
79
  }
75
80
  /**
76
81
  * Start the container and wait for the runner to signal readiness.
@@ -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
  }>;
@@ -3,36 +3,57 @@
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
7
  // ── Availability checks (cached) ────────────────────────────────────
8
8
  let _isolatedVmAvailable = null;
9
9
  async function isIsolatedVmAvailable() {
10
10
  if (_isolatedVmAvailable !== null)
11
11
  return _isolatedVmAvailable;
12
+ logDebug('Checking isolated-vm availability...', { component: 'Executor' });
12
13
  try {
13
- // @ts-ignore - isolated-vm is an optional dependency
14
- await import('isolated-vm');
14
+ // We run the check in a separate process because isolated-vm can cause
15
+ // segmentation faults if native dependencies or environment are incompatible.
16
+ // Running it here ensures a crash doesn't take down the main bridge process.
17
+ const checkScript = `
18
+ import ivm from 'isolated-vm';
19
+ const isolate = new ivm.Isolate({ memoryLimit: 8 });
20
+ isolate.dispose();
21
+ process.exit(0);
22
+ `;
23
+ const { execSync } = await import('node:child_process');
24
+ execSync(`node -e "${checkScript.replace(/"/g, '\\"').replace(/\n/g, '')}"`, { stdio: 'ignore', timeout: 5000 });
25
+ logDebug('isolated-vm is available and functional (verified via subprocess)', { component: 'Executor' });
15
26
  _isolatedVmAvailable = true;
16
27
  }
17
- catch {
28
+ catch (err) {
29
+ logDebug(`isolated-vm check failed or crashed: ${err instanceof Error ? err.message : String(err)}`, { component: 'Executor' });
18
30
  _isolatedVmAvailable = false;
19
31
  }
20
32
  return _isolatedVmAvailable;
21
33
  }
22
34
  let _containerRuntimeAvailable = null;
23
- function isContainerRuntimeAvailable() {
35
+ async function isContainerRuntimeAvailable() {
24
36
  if (_containerRuntimeAvailable !== null)
25
37
  return _containerRuntimeAvailable;
38
+ logDebug('Checking container runtime availability...', { component: 'Executor' });
39
+ // Check for docker or podman
26
40
  for (const cmd of ['docker', 'podman']) {
27
41
  try {
28
- execFileSync(cmd, ['--version'], { stdio: 'ignore', timeout: 5000 });
42
+ logDebug(`Testing container runtime: ${cmd}`, { component: 'Executor' });
43
+ // Use 'ps' instead of '--version' because 'ps' requires the
44
+ // runtime daemon/service to be actually running and responsive.
45
+ // execFileSync is okay here as it's a one-time check during startup/first-use.
46
+ execFileSync(cmd, ['ps'], { stdio: 'ignore', timeout: 3000 });
47
+ logDebug(`Container runtime "${cmd}" is available and responsive`, { component: 'Executor' });
29
48
  _containerRuntimeAvailable = true;
30
49
  return true;
31
50
  }
32
- catch {
33
- // not available
51
+ catch (err) {
52
+ logDebug(`Container runtime "${cmd}" check failed: ${err instanceof Error ? err.message : String(err)}`, { component: 'Executor' });
53
+ // not available or not running
34
54
  }
35
55
  }
56
+ logDebug('No responsive container runtime found', { component: 'Executor' });
36
57
  _containerRuntimeAvailable = false;
37
58
  return false;
38
59
  }
@@ -50,7 +71,7 @@ const executorRegistry = [
50
71
  {
51
72
  type: 'container',
52
73
  preference: 1,
53
- isAvailable: async () => isContainerRuntimeAvailable(),
74
+ isAvailable: isContainerRuntimeAvailable,
54
75
  async create(timeout) {
55
76
  const { createContainerExecutor } = await import('../executor/container-executor.js');
56
77
  return createContainerExecutor({ timeout });
@@ -79,26 +100,27 @@ const executorRegistry = [
79
100
  * Factory function to create an Executor instance.
80
101
  *
81
102
  * Selection logic:
103
+ * - If explicitType is provided, that executor is used (throws if unavailable).
82
104
  * - If EXECUTOR_TYPE is set, that executor is used (throws if unavailable).
83
105
  * - Otherwise, executors are tried in preference order (isolated-vm →
84
106
  * container → vm2) and the first available one is selected.
85
107
  *
86
108
  * Returns both the executor and metadata about the selection.
87
109
  */
88
- export async function createExecutor(timeout = 30000) {
89
- const requested = process.env.EXECUTOR_TYPE?.toLowerCase();
110
+ export async function createExecutor(timeout = 30000, explicitType) {
111
+ const requested = (explicitType || process.env.EXECUTOR_TYPE?.toLowerCase());
90
112
  // Explicit selection — must succeed or throw
91
113
  if (requested) {
92
114
  const entry = executorRegistry.find(e => e.type === requested);
93
115
  if (!entry) {
94
116
  const known = executorRegistry.map(e => e.type).join(', ');
95
- throw new Error(`Unknown EXECUTOR_TYPE="${requested}". Valid types: ${known}`);
117
+ throw new Error(`Unknown executor type "${requested}". Valid types: ${known}`);
96
118
  }
97
119
  const available = await entry.isAvailable();
98
120
  if (!available) {
99
- throw new Error(`EXECUTOR_TYPE=${requested} but it is not available in this environment.`);
121
+ throw new Error(`Executor type ${requested} requested but it is not available in this environment.`);
100
122
  }
101
- logInfo(`Using ${entry.type} executor (EXECUTOR_TYPE=${requested})`, { component: 'Executor' });
123
+ logInfo(`Using ${entry.type} executor (${explicitType ? 'explicit option' : `EXECUTOR_TYPE=${requested}`})`, { component: 'Executor' });
102
124
  return {
103
125
  executor: await entry.create(timeout),
104
126
  info: { type: entry.type, reason: 'explicit', timeout },
@@ -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>;
@@ -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' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ruifung/codemode-bridge",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
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": {