From ff7ee7cd7b873d8f3e4a388be2443a3f626d9a9d Mon Sep 17 00:00:00 2001 From: SWSAmor Date: Sun, 26 Apr 2026 09:16:27 +0200 Subject: [PATCH 1/2] feat(build): single-binary distribution via bun --compile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `bun build --compile` build scripts producing standalone binaries for darwin-arm64/x64, linux-x64/arm64, windows-x64 (~63MB host target) - Embed template files via auto-generated `src/cli/templates/embedded.ts`, with filesystem-first / embedded-fallback loader so npm/npx/dev still reads from `templates/` and the compiled binary uses inline strings - Lazy-load pyodide (13MB WASM) via dynamic import — keeps it out of the compiled binary unless `PYTHON_SANDBOX_READY=true` enables it at runtime - Skip `npm install -g` self-bootstrap when running as compiled binary (new `runtime-detect.ts` heuristic on `process.execPath`) - Add `.github/workflows/release.yml` that builds the 5-target matrix on tag push and attaches binaries + SHA256SUMS to a GitHub Release - README: document npm / bunx / standalone-binary install paths --- .github/workflows/release.yml | 110 ++++++++++++++++++++++++++++++ .gitignore | 2 + README.md | 21 +++++- package.json | 8 +++ scripts/embed-templates.ts | 58 ++++++++++++++++ src/cli/runtime-detect.ts | 23 +++++++ src/cli/self-installer.ts | 8 ++- src/cli/templates/embedded.ts | 15 ++++ src/cli/templates/loader.ts | 39 +++++++++++ src/cli/wizard.ts | 6 +- src/cli/wrapper-generator.ts | 17 ++--- src/executors/pyodide-executor.ts | 5 +- src/index.ts | 19 ++++-- 13 files changed, 308 insertions(+), 23 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 scripts/embed-templates.ts create mode 100644 src/cli/runtime-detect.ts create mode 100644 src/cli/templates/embedded.ts create mode 100644 src/cli/templates/loader.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1671c15 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,110 @@ +name: Release Binaries + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + tag: + description: 'Tag to build (e.g. v1.0.6)' + required: true + +jobs: + build: + name: Build ${{ matrix.target }} + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - target: darwin-arm64 + runner: macos-14 + outfile: code-executor-mcp-darwin-arm64 + bun_target: bun-darwin-arm64 + - target: darwin-x64 + runner: macos-13 + outfile: code-executor-mcp-darwin-x64 + bun_target: bun-darwin-x64 + - target: linux-x64 + runner: ubuntu-latest + outfile: code-executor-mcp-linux-x64 + bun_target: bun-linux-x64 + - target: linux-arm64 + runner: ubuntu-22.04-arm + outfile: code-executor-mcp-linux-arm64 + bun_target: bun-linux-arm64 + - target: windows-x64 + runner: windows-latest + outfile: code-executor-mcp-windows-x64.exe + bun_target: bun-windows-x64 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Embed templates + run: bun run embed-templates + + - name: Build binary + shell: bash + run: | + mkdir -p bin + bun build --compile --minify --sourcemap --external pyodide \ + --target=${{ matrix.bun_target }} \ + src/index.ts \ + --outfile=bin/${{ matrix.outfile }} + + - name: Smoke test (host targets only) + if: matrix.target == 'darwin-arm64' || matrix.target == 'linux-x64' + shell: bash + run: | + ./bin/${{ matrix.outfile }} --help 2>&1 | head -5 || true + file ./bin/${{ matrix.outfile }} + ls -lh ./bin/${{ matrix.outfile }} + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.outfile }} + path: bin/${{ matrix.outfile }} + if-no-files-found: error + + release: + name: Create GitHub Release + needs: build + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') + permissions: + contents: write + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Flatten artifact directories + run: | + mkdir -p release + find artifacts -type f -exec cp {} release/ \; + ls -lh release/ + + - name: Create checksums + working-directory: release + run: | + shasum -a 256 * > SHA256SUMS + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: release/* + generate_release_notes: true + fail_on_unmatched_files: true diff --git a/.gitignore b/.gitignore index 80b326b..bcdebf0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ node_modules/ # Build output dist/ +bin/ +.*.bun-build # Test coverage coverage/ diff --git a/README.md b/README.md index 45b4bf8..d570ed5 100644 --- a/README.md +++ b/README.md @@ -81,13 +81,32 @@ Traditional MCP exposes all 47 tools upfront (141k tokens). Code Executor expose ### Option 1: Interactive Setup Wizard (Recommended) -Don't configure manually. Our wizard does everything: +Don't configure manually. Our wizard does everything. +**Via npm (Node.js 22+):** ```bash npm install -g code-executor-mcp code-executor-mcp setup ``` +**Via bun (Bun 1.1+):** +```bash +bunx code-executor-mcp setup +``` + +**Via standalone binary (no Node.js or Bun required):** + +Download the binary for your platform from the [Releases page](https://github.com/aberemia24/code-executor-MCP/releases) and run it directly: +```bash +# macOS (Apple Silicon) +curl -L -o code-executor-mcp https://github.com/aberemia24/code-executor-MCP/releases/latest/download/code-executor-mcp-darwin-arm64 +chmod +x code-executor-mcp +./code-executor-mcp setup +``` +Available targets: `darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`, `windows-x64`. + +The binary still needs **Deno** (for TypeScript sandbox) and **Python 3** (for native Python sandbox) on `PATH`. The setup wizard verifies these and shows install instructions if missing. + **What the wizard does:** 1. 🔍 Scans for existing MCP configs (Claude Code `~/.claude.json`, Cursor `~/.cursor/mcp.json`, project `.mcp.json`) 2. ⚙️ Configures with smart defaults (or customize interactively) diff --git a/package.json b/package.json index d70e09c..abeac17 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,14 @@ "scripts": { "build": "tsc", "dev": "tsc --watch", + "embed-templates": "bun run scripts/embed-templates.ts", + "build:binary": "bun run embed-templates && bun build --compile --minify --sourcemap --external pyodide src/index.ts --outfile=bin/code-executor-mcp", + "build:binary:darwin-arm64": "bun run embed-templates && bun build --compile --minify --sourcemap --external pyodide --target=bun-darwin-arm64 src/index.ts --outfile=bin/code-executor-mcp-darwin-arm64", + "build:binary:darwin-x64": "bun run embed-templates && bun build --compile --minify --sourcemap --external pyodide --target=bun-darwin-x64 src/index.ts --outfile=bin/code-executor-mcp-darwin-x64", + "build:binary:linux-x64": "bun run embed-templates && bun build --compile --minify --sourcemap --external pyodide --target=bun-linux-x64 src/index.ts --outfile=bin/code-executor-mcp-linux-x64", + "build:binary:linux-arm64": "bun run embed-templates && bun build --compile --minify --sourcemap --external pyodide --target=bun-linux-arm64 src/index.ts --outfile=bin/code-executor-mcp-linux-arm64", + "build:binary:windows-x64": "bun run embed-templates && bun build --compile --minify --sourcemap --external pyodide --target=bun-windows-x64 src/index.ts --outfile=bin/code-executor-mcp-windows-x64.exe", + "build:binary:all": "npm run build:binary:darwin-arm64 && npm run build:binary:darwin-x64 && npm run build:binary:linux-x64 && npm run build:binary:linux-arm64 && npm run build:binary:windows-x64", "start": "node dist/index.js", "server": "npm run build && node dist/index.js", "generate-wrappers": "npm run build && node dist/wrapper-generator.js", diff --git a/scripts/embed-templates.ts b/scripts/embed-templates.ts new file mode 100644 index 0000000..e3d6401 --- /dev/null +++ b/scripts/embed-templates.ts @@ -0,0 +1,58 @@ +/** + * Generate src/cli/templates/embedded.ts from templates/* files. + * + * Templates ship two ways: + * 1. As loose files under `templates/` (npm/npx/bunx install) — preferred path + * 2. Embedded as string constants in the binary (bun --compile distribution) + * + * This script regenerates the embedded constants whenever templates change. + * Run via `npm run embed-templates` (wired into prebuild). + */ + +import { readFile, writeFile, readdir } from 'fs/promises'; +import { fileURLToPath } from 'url'; +import * as path from 'path'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const TEMPLATES_DIR = path.resolve(__dirname, '..', 'templates'); +const OUTPUT_PATH = path.resolve(__dirname, '..', 'src', 'cli', 'templates', 'embedded.ts'); + +async function main(): Promise { + const files = await readdir(TEMPLATES_DIR); + const entries: Array<[string, string]> = []; + + for (const filename of files.sort()) { + const filePath = path.join(TEMPLATES_DIR, filename); + const content = await readFile(filePath, 'utf-8'); + entries.push([filename, content]); + } + + const lines: string[] = [ + '/**', + ' * Auto-generated by scripts/embed-templates.ts. Do not edit by hand.', + ' *', + ' * Templates embedded as string constants for the bun --compile single-binary', + ' * distribution where templates/ is not on disk. The filesystem path is', + ' * preferred when available (npm install).', + ' */', + '', + 'export const EMBEDDED_TEMPLATES = {', + ]; + + for (const [name, content] of entries) { + lines.push(` ${JSON.stringify(name)}: ${JSON.stringify(content)},`); + } + + lines.push('} as const;'); + lines.push(''); + lines.push('export type EmbeddedTemplateName = keyof typeof EMBEDDED_TEMPLATES;'); + lines.push(''); + + await writeFile(OUTPUT_PATH, lines.join('\n'), 'utf-8'); + console.log(`Embedded ${entries.length} templates → ${path.relative(process.cwd(), OUTPUT_PATH)}`); +} + +main().catch((err: unknown) => { + console.error(err); + process.exit(1); +}); diff --git a/src/cli/runtime-detect.ts b/src/cli/runtime-detect.ts new file mode 100644 index 0000000..09ee29a --- /dev/null +++ b/src/cli/runtime-detect.ts @@ -0,0 +1,23 @@ +/** + * Runtime detection helpers. + * + * Used to gate features that don't make sense in a compiled single-binary + * distribution (e.g., self-installation via `npm install -g`). + */ + +declare const Bun: { version?: string } | undefined; + +/** + * True when running as a `bun --compile` compiled binary. + * + * Heuristic: Bun is defined and `process.execPath` is not the `bun` runtime + * itself — in compiled mode it points to the produced binary instead. + * + * Override with `CODE_EXECUTOR_FORCE_COMPILED=1` for testing. + */ +export function isCompiledBinary(): boolean { + if (process.env.CODE_EXECUTOR_FORCE_COMPILED === '1') return true; + if (typeof Bun === 'undefined') return false; + const exec = process.execPath.toLowerCase(); + return !exec.endsWith('/bun') && !exec.endsWith('\\bun.exe') && !exec.endsWith('bun'); +} diff --git a/src/cli/self-installer.ts b/src/cli/self-installer.ts index 6ec130a..3b542eb 100644 --- a/src/cli/self-installer.ts +++ b/src/cli/self-installer.ts @@ -1,4 +1,5 @@ import { spawn } from 'child_process'; +import { isCompiledBinary } from './runtime-detect.js'; /** * SelfInstaller - Detects and installs code-executor-mcp globally @@ -108,8 +109,13 @@ Or configure npm to use a user-writable directory: https://docs.npmjs.com/resolv * @throws Error with remediation message if installation fails */ async runBootstrap(): Promise { + if (isCompiledBinary()) { + console.log('📦 Running as compiled binary — skipping npm self-install.'); + return; + } + console.log('🔍 Checking if code-executor-mcp is globally installed...'); - + const isInstalled = await this.detectGlobalInstall(); if (isInstalled) { diff --git a/src/cli/templates/embedded.ts b/src/cli/templates/embedded.ts new file mode 100644 index 0000000..17bd9b9 --- /dev/null +++ b/src/cli/templates/embedded.ts @@ -0,0 +1,15 @@ +/** + * Auto-generated by scripts/embed-templates.ts. Do not edit by hand. + * + * Templates embedded as string constants for the bun --compile single-binary + * distribution where templates/ is not on disk. The filesystem path is + * preferred when available (npm install). + */ + +export const EMBEDDED_TEMPLATES = { + "python-wrapper.hbs": "\"\"\"\nGenerated MCP Wrapper: {{mcpName}}\n\nThis file was auto-generated by code-executor-mcp.\nDO NOT EDIT MANUALLY - changes will be overwritten on next generation.\n\nGenerated: {{generatedAt}}\nSchema Hash: {{schemaHash}}\nTool Count: {{toolCount}}\n\nSee: https://github.com/aberemia24/code-executor-MCP\n\"\"\"\n\nfrom typing import Any, Dict, Optional, List, TypedDict\nimport json\nimport os\nimport requests\n\nclass ExecutionResult(TypedDict):\n \"\"\"Result from MCP tool execution\"\"\"\n success: bool\n output: str\n error: Optional[str]\n executionTimeMs: int\n toolCallsMade: Optional[List[str]]\n\n\nclass {{pascalCase mcpName}}Client:\n \"\"\"\n MCP Server: {{mcpName}}\n {{#if description}}\n Description: {{description}}\n {{/if}}\n\n Available Tools: {{toolCount}}\n \"\"\"\n\n def __init__(\n self,\n proxy_url: Optional[str] = None,\n auth_token: Optional[str] = None\n ):\n \"\"\"\n Initialize {{mcpName}} MCP client.\n\n Args:\n proxy_url: URL of code-executor-mcp proxy server\n (defaults to CODE_EXECUTOR_PROXY_URL env var or http://localhost:3000)\n auth_token: Bearer token for authentication\n (defaults to CODE_EXECUTOR_AUTH_TOKEN env var, required)\n\n Raises:\n ValueError: If auth_token is not provided and CODE_EXECUTOR_AUTH_TOKEN is not set\n \"\"\"\n self.proxy_url = proxy_url or os.getenv('CODE_EXECUTOR_PROXY_URL', 'http://localhost:3000')\n self.auth_token = auth_token or os.getenv('CODE_EXECUTOR_AUTH_TOKEN')\n\n if not self.auth_token:\n raise ValueError('auth_token must be provided or CODE_EXECUTOR_AUTH_TOKEN environment variable must be set')\n\n def _call_mcp(self, tool_name: str, params: Dict[str, Any], timeout_ms: int = 30000) -> ExecutionResult:\n \"\"\"\n Internal method to call MCP tool via code-executor proxy.\n\n Args:\n tool_name: MCP tool name\n params: Tool parameters\n timeout_ms: Execution timeout in milliseconds\n\n Returns:\n ExecutionResult with output and metadata\n\n Raises:\n requests.HTTPError: If MCP call fails\n \"\"\"\n code = f\"\"\"\nconst result = await callMCPTool('{tool_name}', {json.dumps(params)});\nconsole.log(JSON.stringify(result));\n\"\"\"\n\n response = requests.post(\n self.proxy_url,\n json={\n \"toolName\": \"executeTypescript\",\n \"params\": {\n \"code\": code,\n \"allowedTools\": [tool_name],\n \"timeoutMs\": timeout_ms\n }\n },\n headers={\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {self.auth_token}\"\n }\n )\n\n response.raise_for_status()\n return response.json()\n\n {{#each tools}}\n def {{snakeCase (camelCase this.name)}}(\n self,\n {{#if this.inputSchema.properties}}\n {{#each this.inputSchema.properties}}\n {{snakeCase @key}}: {{pythonType this.type}}{{#unless (lookup ../this.inputSchema.required @key)}} = None{{/unless}},\n {{/each}}\n {{/if}}\n ) -> ExecutionResult:\n \"\"\"\n {{this.description}}\n\n Tool: {{this.name}}\n\n {{#if this.inputSchema.properties}}\n Args:\n {{#each this.inputSchema.properties}}\n {{snakeCase @key}}: {{#if this.description}}{{this.description}}{{else}}{{this.type}} parameter{{/if}}{{#if (lookup ../this.inputSchema.required @key)}} (required){{/if}}\n {{/each}}\n {{/if}}\n\n Returns:\n ExecutionResult with output and metadata\n\n Example:\n >>> client = {{pascalCase ../mcpName}}Client()\n >>> result = client.{{snakeCase (camelCase this.name)}}(\n {{#if this.inputSchema.properties}}\n {{#each this.inputSchema.properties}}\n ... {{snakeCase @key}}={{#if (eq this.type \"string\")}}'example'{{else if (eq this.type \"number\")}}123{{else if (eq this.type \"boolean\")}}True{{else if (eq this.type \"array\")}}[]{{else}}{}{{/if}},\n {{/each}}\n {{/if}}\n ... )\n >>> print(result['output'])\n \"\"\"\n params = {\n {{#each this.inputSchema.properties}}\n '{{@key}}': {{snakeCase @key}},\n {{/each}}\n }\n\n # Remove None values for optional parameters\n params = {k: v for k, v in params.items() if v is not None}\n\n return self._call_mcp('{{this.name}}', params)\n\n {{/each}}\n\n\n# Convenience functions for direct usage\n{{#each tools}}\ndef {{snakeCase (camelCase this.name)}}(\n {{#if this.inputSchema.properties}}\n {{#each this.inputSchema.properties}}\n {{snakeCase @key}}: {{pythonType this.type}}{{#unless (lookup ../this.inputSchema.required @key)}} = None{{/unless}},\n {{/each}}\n {{/if}}\n proxy_url: Optional[str] = None,\n auth_token: Optional[str] = None\n) -> ExecutionResult:\n \"\"\"\n Convenience function for {{this.name}}.\n\n {{this.description}}\n\n {{#if this.inputSchema.properties}}\n Args:\n {{#each this.inputSchema.properties}}\n {{snakeCase @key}}: {{#if this.description}}{{this.description}}{{else}}{{this.type}} parameter{{/if}}{{#if (lookup ../this.inputSchema.required @key)}} (required){{/if}}\n {{/each}}\n {{/if}}\n proxy_url: URL of code-executor-mcp proxy server\n (defaults to CODE_EXECUTOR_PROXY_URL env var or http://localhost:3000)\n auth_token: Bearer token for authentication\n (defaults to CODE_EXECUTOR_AUTH_TOKEN env var, required)\n\n Returns:\n ExecutionResult with output and metadata\n\n Raises:\n ValueError: If auth_token is not provided and CODE_EXECUTOR_AUTH_TOKEN is not set\n \"\"\"\n client = {{pascalCase ../mcpName}}Client(proxy_url, auth_token)\n return client.{{snakeCase (camelCase this.name)}}(\n {{#each this.inputSchema.properties}}\n {{snakeCase @key}}={{snakeCase @key}},\n {{/each}}\n )\n\n{{/each}}\n", + "typescript-wrapper.hbs": "/**\n * Generated MCP Wrapper: {{mcpName}}\n *\n * This file was auto-generated by code-executor-mcp.\n * DO NOT EDIT MANUALLY - changes will be overwritten on next generation.\n *\n * Generated: {{generatedAt}}\n * Schema Hash: {{schemaHash}}\n * Tool Count: {{toolCount}}\n *\n * @see https://github.com/aberemia24/code-executor-MCP\n */\n\n{{#if moduleFormat}}\n{{#if (eq moduleFormat \"esm\")}}\nimport type { ExecutionResult, ToolCallSummaryEntry } from 'code-executor-mcp';\n{{else}}\nconst { ExecutionResult, ToolCallSummaryEntry } = require('code-executor-mcp');\n{{/if}}\n{{else}}\nimport type { ExecutionResult, ToolCallSummaryEntry } from 'code-executor-mcp';\n{{/if}}\n\n/**\n * MCP Server: {{mcpName}}\n * {{#if description}}\n * Description: {{description}}\n * {{/if}}\n *\n * Available Tools: {{toolCount}}\n */\n\n{{#each tools}}\n/**\n * {{this.description}}\n *\n * Tool: {{this.name}}\n *\n {{#if this.inputSchema.properties}}\n * Parameters:\n {{#each this.inputSchema.properties}}\n * @param {{@key}}{{#if this.description}} - {{this.description}}{{/if}}{{#if (lookup ../this.inputSchema.required @key)}} (required){{/if}}\n {{/each}}\n {{/if}}\n *\n * @returns Promise - Execution result with output and metadata\n *\n * @example\n * ```typescript\n * const result = await {{../mcpName}}_{{camelCase this.name}}({\n {{#if this.inputSchema.properties}}\n {{#each this.inputSchema.properties}}\n * {{@key}}: {{#if (eq this.type \"string\")}}'example'{{else if (eq this.type \"number\")}}123{{else if (eq this.type \"boolean\")}}true{{else if (eq this.type \"array\")}}[]{{else}}{}{{/if}},\n {{/each}}\n {{/if}}\n * });\n * console.log(result.output);\n * ```\n */\nexport async function {{../mcpName}}_{{camelCase this.name}}(\n params: {\n {{#each this.inputSchema.properties}}\n {{@key}}{{#unless (lookup ../this.inputSchema.required @key)}}?{{/unless}}: {{tsType this.type}};\n {{/each}}\n }\n): Promise {\n // Call MCP tool via code-executor\n const code = `\n const result = await callMCPTool('{{this.name}}', ` + JSON.stringify(params) + `);\n console.log(JSON.stringify(result));\n `;\n\n // Execute via code-executor-mcp executeTypescript\n const proxyUrl = process.env.CODE_EXECUTOR_PROXY_URL || 'http://localhost:3000';\n const authToken = process.env.CODE_EXECUTOR_AUTH_TOKEN;\n\n if (!authToken) {\n throw new Error('CODE_EXECUTOR_AUTH_TOKEN environment variable is required');\n }\n\n const response = await fetch(proxyUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${authToken}`\n },\n body: JSON.stringify({\n toolName: 'executeTypescript',\n params: {\n code,\n allowedTools: ['{{this.name}}'],\n timeoutMs: 30000\n }\n })\n });\n\n if (!response.ok) {\n throw new Error(`MCP call failed: ${response.statusText}`);\n }\n\n return await response.json();\n}\n\n{{/each}}\n\n/**\n * Type definitions for {{mcpName}} MCP server\n */\nexport namespace {{pascalCase mcpName}} {\n {{#each tools}}\n export interface {{pascalCase this.name}}Params {\n {{#each this.inputSchema.properties}}\n {{@key}}{{#unless (lookup ../this.inputSchema.required @key)}}?{{/unless}}: {{tsType this.type}};\n {{/each}}\n }\n\n {{/each}}\n}\n\n{{#if moduleFormat}}\n{{#if (eq moduleFormat \"commonjs\")}}\n// CommonJS export\nmodule.exports = {\n {{#each tools}}\n {{../mcpName}}_{{camelCase this.name}},\n {{/each}}\n};\n{{/if}}\n{{/if}}\n", + "vscode-tasks.json": "{\n \"version\": \"2.0.0\",\n \"tasks\": [\n {\n \"label\": \"Code Executor MCP: Daily Sync\",\n \"type\": \"shell\",\n \"command\": \"npx\",\n \"args\": [\n \"code-executor-mcp\",\n \"sync-wrappers\"\n ],\n \"problemMatcher\": [],\n \"presentation\": {\n \"reveal\": \"always\",\n \"panel\": \"dedicated\",\n \"clear\": true,\n \"showReuseMessage\": false\n },\n \"runOptions\": {\n \"runOn\": \"default\"\n },\n \"group\": {\n \"kind\": \"build\",\n \"isDefault\": false\n },\n \"detail\": \"Manually trigger MCP wrapper regeneration (checks for schema changes)\",\n \"icon\": {\n \"id\": \"sync\",\n \"color\": \"terminal.ansiBlue\"\n }\n },\n {\n \"label\": \"Code Executor MCP: Force Regenerate All\",\n \"type\": \"shell\",\n \"command\": \"npx\",\n \"args\": [\n \"code-executor-mcp\",\n \"sync-wrappers\",\n \"--force\"\n ],\n \"problemMatcher\": [],\n \"presentation\": {\n \"reveal\": \"always\",\n \"panel\": \"dedicated\",\n \"clear\": true,\n \"showReuseMessage\": false\n },\n \"runOptions\": {\n \"runOn\": \"default\"\n },\n \"group\": {\n \"kind\": \"build\",\n \"isDefault\": false\n },\n \"detail\": \"Force regenerate ALL wrappers (ignores schema hashes)\",\n \"icon\": {\n \"id\": \"refresh\",\n \"color\": \"terminal.ansiYellow\"\n }\n },\n {\n \"label\": \"Code Executor MCP: Setup Wizard\",\n \"type\": \"shell\",\n \"command\": \"npx\",\n \"args\": [\n \"code-executor-mcp\",\n \"setup\"\n ],\n \"problemMatcher\": [],\n \"presentation\": {\n \"reveal\": \"always\",\n \"panel\": \"dedicated\",\n \"clear\": true,\n \"showReuseMessage\": false\n },\n \"runOptions\": {\n \"runOn\": \"default\"\n },\n \"group\": {\n \"kind\": \"build\",\n \"isDefault\": false\n },\n \"detail\": \"Run interactive CLI setup wizard\",\n \"icon\": {\n \"id\": \"wand\",\n \"color\": \"terminal.ansiMagenta\"\n }\n }\n ]\n}\n", +} as const; + +export type EmbeddedTemplateName = keyof typeof EMBEDDED_TEMPLATES; diff --git a/src/cli/templates/loader.ts b/src/cli/templates/loader.ts new file mode 100644 index 0000000..4e6c0ff --- /dev/null +++ b/src/cli/templates/loader.ts @@ -0,0 +1,39 @@ +/** + * Template loader with filesystem-first, embedded-fallback strategy. + * + * Filesystem path: used when templates ship alongside the package + * (npm/npx/bunx install + dev workflow). + * + * Embedded fallback: used when running as a `bun --compile` single binary + * where templates/ is not on disk. Constants come from `embedded.ts`, + * regenerated by `scripts/embed-templates.ts` whenever templates change. + */ + +import { readFile } from 'fs/promises'; +import * as path from 'path'; +import { EMBEDDED_TEMPLATES, type EmbeddedTemplateName } from './embedded.js'; + +export type TemplateName = EmbeddedTemplateName; + +/** + * Load a template by filename. If `templateDir` is provided and contains the + * file, returns its filesystem content. Otherwise falls back to the embedded + * constant. + */ +export async function loadTemplate( + name: TemplateName, + templateDir?: string +): Promise { + if (templateDir) { + const fsPath = path.join(templateDir, name); + try { + return await readFile(fsPath, 'utf-8'); + } catch (err: unknown) { + const code = (err as NodeJS.ErrnoException)?.code; + if (code !== 'ENOENT' && code !== 'ENOTDIR') { + throw err; + } + } + } + return EMBEDDED_TEMPLATES[name]; +} diff --git a/src/cli/wizard.ts b/src/cli/wizard.ts index c7ac86e..ae820a9 100644 --- a/src/cli/wizard.ts +++ b/src/cli/wizard.ts @@ -815,14 +815,14 @@ export class CLIWizard { async generateVSCodeTasks(projectRoot: string): Promise { const fs = await import('fs/promises'); const path = await import('path'); + const { loadTemplate } = await import('./templates/loader.js'); // Create .vscode directory if it doesn't exist const vscodeDir = path.join(projectRoot, '.vscode'); await fs.mkdir(vscodeDir, { recursive: true }); - // Read template - const templatePath = path.join(__dirname, '..', '..', 'templates', 'vscode-tasks.json'); - const templateContent = await fs.readFile(templatePath, 'utf8'); + // Load template — embedded constant (small, never edited at runtime) + const templateContent = await loadTemplate('vscode-tasks.json'); const templateTasks = JSON.parse(templateContent); // Check if tasks.json already exists diff --git a/src/cli/wrapper-generator.ts b/src/cli/wrapper-generator.ts index 9919073..a48731f 100644 --- a/src/cli/wrapper-generator.ts +++ b/src/cli/wrapper-generator.ts @@ -20,6 +20,7 @@ import type { ModuleFormat, ToolSchema, } from './types.js'; +import { loadTemplate, type TemplateName } from './templates/loader.js'; /** * WrapperGeneratorOptions - Configuration for WrapperGenerator @@ -361,18 +362,10 @@ export class WrapperGenerator { // Ensure output directory exists await fs.mkdir(languageDir, { recursive: true }); - // Load template - const templatePath = path.join( - this.templateDir, - language === 'typescript' ? 'typescript-wrapper.hbs' : 'python-wrapper.hbs' - ); - - const templateSource = await fs.readFile(templatePath, 'utf-8').catch((error: unknown) => { - if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') { - throw new Error(`Template not found: ${templatePath}`); - } - throw error; - }); + // Load template — filesystem first, embedded fallback for compiled binary + const templateName: TemplateName = + language === 'typescript' ? 'typescript-wrapper.hbs' : 'python-wrapper.hbs'; + const templateSource = await loadTemplate(templateName, this.templateDir); // Compile template with auto-escaping enabled (CVE-2021-23369 mitigation) const template = this.handlebars.compile(templateSource, { diff --git a/src/executors/pyodide-executor.ts b/src/executors/pyodide-executor.ts index 82ffa2b..8a8e8c7 100644 --- a/src/executors/pyodide-executor.ts +++ b/src/executors/pyodide-executor.ts @@ -14,7 +14,9 @@ * Issue #59: Pyodide WebAssembly sandbox for Python execution */ -import { loadPyodide, type PyodideInterface } from 'pyodide'; +// Pyodide is loaded via dynamic import so `bun --compile` binaries that don't +// enable PYTHON_SANDBOX_READY don't pull the 13MB WASM bundle into the binary. +type PyodideInterface = import('pyodide').PyodideInterface; import { Server as McpServer } from '@modelcontextprotocol/sdk/server/index.js'; import { MCPProxyServer } from '../core/server/mcp-proxy-server.js'; import { StreamingProxy } from '../core/middleware/streaming-proxy.js'; @@ -44,6 +46,7 @@ async function getPyodide(): Promise { // Node.js: Use npm package files (no indexURL needed) // The pyodide npm package includes all necessary files locally + const { loadPyodide } = await import('pyodide'); pyodideCache = await loadPyodide({ // SECURITY: Disable stdin to prevent interactive prompts stdin: () => { diff --git a/src/index.ts b/src/index.ts index d8f57c8..03a6a63 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,7 +21,9 @@ import { ConnectionPool } from './mcp/connection-pool.js'; import { RateLimiter } from './security/rate-limiter.js'; import { executeTypescriptInSandbox } from './executors/sandbox-executor.js'; import { executePythonInSandbox as executePythonNative } from './executors/python-executor.js'; -import { executePythonInSandbox as executePythonPyodide } from './executors/pyodide-executor.js'; +// Pyodide is loaded lazily — only imported when PYTHON_SANDBOX_READY=true. +// This keeps the 13MB pyodide WASM out of `bun --compile` binaries that don't need it. +type ExecutePythonPyodideFn = typeof import('./executors/pyodide-executor.js').executePythonInSandbox; import { formatErrorResponse, formatExecutionResultForCli } from './utils/utils.js'; import { ErrorType } from './types.js'; import { checkDenoAvailable, getDenoVersion, getDenoInstallMessage } from './executors/deno-checker.js'; @@ -537,9 +539,14 @@ Example: // Execute code with connection pooling // Use Pyodide (secure) when PYTHON_SANDBOX_READY, otherwise native (insecure) - const executePythonInSandbox = PYTHON_SANDBOX_READY - ? executePythonPyodide - : executePythonNative; + // Pyodide loaded lazily so its 13MB WASM stays out of compiled binaries that don't enable it. + let executePythonInSandbox: ExecutePythonPyodideFn; + if (PYTHON_SANDBOX_READY) { + const pyodideMod = await import('./executors/pyodide-executor.js'); + executePythonInSandbox = pyodideMod.executePythonInSandbox; + } else { + executePythonInSandbox = executePythonNative; + } const result = await this.connectionPool.execute(async () => { return await executePythonInSandbox( @@ -801,7 +808,9 @@ Returns: // Export functions for testing export { executeTypescriptInSandbox as executeTypescript } from './executors/sandbox-executor.js'; -export { executePythonInSandbox as executePython } from './executors/pyodide-executor.js'; +// Pyodide export kept as a type-only re-export at the top of this file; consumers +// who need the runtime should `import('code-executor-mcp/dist/executors/pyodide-executor.js')` +// directly. Avoiding a static re-export keeps pyodide out of `bun --compile` graphs. // Start server const server = new CodeExecutorServer(); From 0bc7ec43cbf01de45931849e773fddffcc6aef16 Mon Sep 17 00:00:00 2001 From: SWSAmor Date: Fri, 15 May 2026 15:52:06 +0200 Subject: [PATCH 2/2] docs(readme): document building binary from source and wiring into mcpServers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an "Installation Options → Build Standalone Binary (from Source)" section covering `npm run build:binary{,:all}`, Bun 1.1+ prerequisite, template embedding step, and a sample mcpServers JSON snippet that points `command` at the compiled binary instead of `npx`/`node`. --- README.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/README.md b/README.md index d570ed5..cb526b9 100644 --- a/README.md +++ b/README.md @@ -738,6 +738,50 @@ Docker deployment automatically generates complete MCP configuration from enviro See [DOCKER_TESTING.md](DOCKER_TESTING.md) for security details and [docker-compose.example.yml](docker-compose.example.yml) for all available configuration options. +### Build Standalone Binary (from Source) + +To produce the single-file binary locally — useful for development, custom patches, or self-contained deployment without npm/Node on `PATH`: + +```bash +git clone https://github.com/aberemia24/code-executor-MCP.git +cd code-executor-MCP +npm install + +# Host target only (current platform) → ./bin/code-executor-mcp (~63 MB) +npm run build:binary + +# Or cross-compile to all five targets: +# bin/code-executor-mcp-darwin-arm64 +# bin/code-executor-mcp-darwin-x64 +# bin/code-executor-mcp-linux-x64 +# bin/code-executor-mcp-linux-arm64 +# bin/code-executor-mcp-windows-x64.exe +npm run build:binary:all +``` + +**Requirements:** Bun 1.1+ on `PATH` (the `npm run` scripts invoke `bun build --compile`). Templates are embedded at build time via `scripts/embed-templates.ts` (auto-run before the compile step). Pyodide is excluded from the binary to keep size down; the npm/`npx` install path keeps Pyodide available if you need it. + +**Wire the binary into `mcpServers`** (replace `npx`/`node` with the absolute path): + +```json +{ + "mcpServers": { + "code-executor": { + "command": "/absolute/path/to/code-executor-MCP/bin/code-executor-mcp", + "args": [], + "env": { + "MCP_CONFIG_PATH": "/full/path/to/.mcp.json", + "DENO_PATH": "/opt/homebrew/bin/deno", + "ENABLE_AUDIT_LOG": "true", + "AUDIT_LOG_PATH": "/Users/you/.code-executor/audit.log" + } + } + } +} +``` + +Compared to `npx -y code-executor-mcp`: no Node.js needed on `PATH`, faster cold-start (no npm resolution/extraction), and a single artifact you can pin and ship alongside your config. + ### Local Development ```bash