From 736604513fd210ec5da0ef28b3fbf793e39262b6 Mon Sep 17 00:00:00 2001 From: mkagenius Date: Fri, 7 Nov 2025 08:41:19 +0530 Subject: [PATCH 1/2] api added along with mcp --- Dockerfile | 4 +- server.py | 198 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 200 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1343aae..b0f8dd2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -100,7 +100,9 @@ EXPOSE 8222 RUN apt-get --fix-broken install # Ensure Node.js, npm (and npx) are set up RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - -RUN apt-get install -y nodejs +RUN apt-get update \ + && apt --fix-broken install -y \ + && apt-get install -y nodejs diff --git a/server.py b/server.py index 7be87a4..0691189 100644 --- a/server.py +++ b/server.py @@ -22,6 +22,8 @@ from playwright.async_api import async_playwright from bs4 import BeautifulSoup import socket +from starlette.requests import Request +from starlette.responses import JSONResponse # --- CONFIGURATION & SETUP --- logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" @@ -731,5 +733,199 @@ async def get_skill_file(skill_name: str, filename: str) -> str: return header + content +# --- REST API ENDPOINTS FOR SANDBOX CLIENT COMPATIBILITY --- +# These endpoints provide REST API access compatible with the instavm SDK client +# allowing local execution without cloud API + +class MockContext: + """Mock context for REST API calls that don't have MCP context""" + async def report_progress(self, progress: int, message: str): + # Log progress instead of reporting through MCP + logger.info(f"Progress {progress}%: {message}") + + # Use the streamable_http_app as it's the modern standard -app = mcp.streamable_http_app() \ No newline at end of file +app = mcp.streamable_http_app() + +# Add custom REST API endpoints compatible with instavm SDK client +async def api_execute(request: Request): + """ + REST API endpoint for executing Python code (compatible with InstaVM SDK). + + Request body (JSON): + { + "command": "print('hello world')", + "session_id": "optional-ignored-for-local", + "language": "python", // optional, only python supported + "timeout": 300 // optional, not used in local execution + } + + Response (JSON): + { + "output": "hello world\\n", + "status": "success" + } + or + { + "output": "", + "error": "error message", + "status": "error" + } + """ + try: + # Parse request body + body = await request.json() + command = body.get("command") + + if not command: + return { + "output": "", + "error": "Missing 'command' field in request body", + "status": "error" + } + + # Create mock context for progress reporting + ctx = MockContext() + + # Execute the code + result = await execute_python_code(command, ctx) + + # Check if result contains an error + if result.startswith("Error:"): + return JSONResponse({ + "output": "", + "error": result, + "status": "error" + }) + + return JSONResponse({ + "output": result, + "status": "success" + }) + + except Exception as e: + logger.error(f"Error in /execute endpoint: {e}", exc_info=True) + return JSONResponse({ + "output": "", + "error": f"Error: {str(e)}", + "status": "error" + }) + + +async def api_browser_navigate(request: Request): + """ + REST API endpoint for browser navigation (compatible with InstaVM SDK). + + Request body (JSON): + { + "url": "https://example.com", + "session_id": "optional-ignored-for-local", + "wait_timeout": 30000 // optional + } + + Response (JSON): + { + "status": "success", + "url": "https://example.com", + "title": "Example Domain" + } + or + { + "status": "error", + "error": "error message" + } + """ + try: + # Parse request body + body = await request.json() + url = body.get("url") + + if not url: + return JSONResponse({ + "status": "error", + "error": "Missing 'url' field in request body" + }) + + # Navigate and get text + result = await navigate_and_get_all_visible_text(url) + + # Check if result contains an error + if result.startswith("Error:"): + return JSONResponse({ + "status": "error", + "error": result + }) + + return JSONResponse({ + "status": "success", + "url": url, + "content": result, + "title": "Navigation successful" + }) + + except Exception as e: + logger.error(f"Error in /v1/browser/interactions/navigate endpoint: {e}", exc_info=True) + return JSONResponse({ + "status": "error", + "error": f"Error: {str(e)}" + }) + + +async def api_browser_extract_content(request: Request): + """ + REST API endpoint for extracting browser content (compatible with InstaVM SDK). + + Request body (JSON): + { + "session_id": "optional-ignored-for-local", + "url": "https://example.com", // required for local execution + "include_interactive": true, + "include_anchors": true, + "max_anchors": 50 + } + + Response (JSON): + { + "readable_content": {"content": "text content"}, + "status": "success" + } + """ + try: + # Parse request body + body = await request.json() + url = body.get("url") + + if not url: + return JSONResponse({ + "status": "error", + "error": "Missing 'url' field in request body (required for local execution)" + }) + + # Navigate and get text + result = await navigate_and_get_all_visible_text(url) + + # Check if result contains an error + if result.startswith("Error:"): + return JSONResponse({ + "status": "error", + "error": result + }) + + return JSONResponse({ + "readable_content": { + "content": result + }, + "status": "success" + }) + + except Exception as e: + logger.error(f"Error in /v1/browser/interactions/content endpoint: {e}", exc_info=True) + return JSONResponse({ + "status": "error", + "error": f"Error: {str(e)}" + }) + +# Add routes to the Starlette app +app.add_route("/execute", api_execute, methods=["POST"]) +app.add_route("/v1/browser/interactions/navigate", api_browser_navigate, methods=["POST"]) +app.add_route("/v1/browser/interactions/content", api_browser_extract_content, methods=["POST"]) \ No newline at end of file From 9eea75f60a9e7f9553f63e54c60b43e07e727d99 Mon Sep 17 00:00:00 2001 From: mkagenius Date: Tue, 27 Jan 2026 22:27:46 +0530 Subject: [PATCH 2/2] fix /execute to be compatible with instavm sdk; add claude code plugin --- Dockerfile | 5 +- README.md | 46 +++- install.sh | 2 +- .../.claude-plugin/marketplace.json | 13 + .../.claude-plugin/plugin.json | 10 + instavm-coderunner-plugin/.mcp.json | 8 + instavm-coderunner-plugin/README.md | 258 ++++++++++++++++++ instavm-coderunner-plugin/install.sh | 46 ++++ .../scripts/mcp-proxy.py | 133 +++++++++ server.py | 147 ++++++++-- 10 files changed, 634 insertions(+), 34 deletions(-) create mode 100644 instavm-coderunner-plugin/.claude-plugin/marketplace.json create mode 100644 instavm-coderunner-plugin/.claude-plugin/plugin.json create mode 100644 instavm-coderunner-plugin/.mcp.json create mode 100644 instavm-coderunner-plugin/README.md create mode 100755 instavm-coderunner-plugin/install.sh create mode 100755 instavm-coderunner-plugin/scripts/mcp-proxy.py diff --git a/Dockerfile b/Dockerfile index b0f8dd2..726c847 100644 --- a/Dockerfile +++ b/Dockerfile @@ -97,12 +97,9 @@ EXPOSE 8222 # Start the FastAPI application # CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8002", "--workers", "1", "--no-access-log"] -RUN apt-get --fix-broken install # Ensure Node.js, npm (and npx) are set up RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - -RUN apt-get update \ - && apt --fix-broken install -y \ - && apt-get install -y nodejs +RUN apt-get update && apt-get install -y nodejs diff --git a/README.md b/README.md index 4dabf1a..990b499 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,41 @@ pip install -r examples/requirements.txt You can now ask Claude to execute code, and it will run safely in the sandbox! -### Option 2: Python OpenAI Agents +### Option 2: Claude Code CLI + +
+Use CodeRunner with Claude Code CLI for terminal-based AI assistance: + +![Claude Code Demo](images/claude-code-demo.png) + +**Quick Start:** + +```bash +# 1. Install and start CodeRunner (one-time setup) +git clone https://github.com/instavm/coderunner.git +cd coderunner +sudo ./install.sh + +# 2. Install the Claude Code plugin +claude plugin marketplace add github:BandarLabs/coderunner/instavm-coderunner-plugin +claude plugin install instavm-coderunner@instavm-plugins + +# 3. Reconnect to MCP servers +/mcp +``` + +That's it! Claude Code now has access to all CodeRunner tools: +- **execute_python_code** - Run Python code in persistent Jupyter kernel +- **navigate_and_get_all_visible_text** - Web scraping with Playwright +- **list_skills** - List available skills (docx, xlsx, pptx, pdf, image processing, etc.) +- **get_skill_info** - Get documentation for specific skills +- **get_skill_file** - Read skill files and examples + +**Learn more:** See [instavm-coderunner-plugin/README.md](instavm-coderunner-plugin/README.md) for detailed documentation. + +
+ +### Option 3: Python OpenAI Agents
Use CodeRunner with OpenAI's Python agents library: @@ -106,7 +140,7 @@ pip install -r examples/requirements.txt Enter prompts like "write python code to generate 100 prime numbers" and watch it execute safely in the sandbox!
-### Option 3: Gemini-CLI +### Option 4: Gemini-CLI [Gemini CLI](https://github.com/google-gemini/gemini-cli) is recently launched by Google.
@@ -132,7 +166,7 @@ pip install -r examples/requirements.txt ![gemini2](images/gemini2.png) -### Option 4: Kiro by Amazon +### Option 5: Kiro by Amazon [Kiro](https://kiro.dev/blog/introducing-kiro/) is recently launched by Amazon.
@@ -161,7 +195,7 @@ pip install -r examples/requirements.txt
-### Option 5: Coderunner-UI (Offline AI Workspace) +### Option 6: Coderunner-UI (Offline AI Workspace) [Coderunner-UI](https://github.com/instavm/coderunner-ui) is our own offline AI workspace tool designed for full privacy and local processing.
@@ -256,6 +290,10 @@ The `examples/` directory contains: - `openai-agents` - Example OpenAI agents integration - `claude-desktop` - Example Claude Desktop integration +## Building Container Image Tutorial + +https://github.com/apple/container/blob/main/docs/tutorial.md + ## Contributing We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. diff --git a/install.sh b/install.sh index 93d4047..8f02327 100755 --- a/install.sh +++ b/install.sh @@ -19,7 +19,7 @@ else echo "✅ macOS system detected." fi -download_url="https://github.com/apple/container/releases/download/0.5.0/container-0.5.0-installer-signed.pkg" +download_url="https://github.com/apple/container/releases/download/0.8.0/container-installer-signed.pkg" # Check if container is installed and display its version if command -v container &> /dev/null diff --git a/instavm-coderunner-plugin/.claude-plugin/marketplace.json b/instavm-coderunner-plugin/.claude-plugin/marketplace.json new file mode 100644 index 0000000..c081e37 --- /dev/null +++ b/instavm-coderunner-plugin/.claude-plugin/marketplace.json @@ -0,0 +1,13 @@ +{ + "name": "instavm-plugins", + "owner": { + "name": "InstaVM" + }, + "plugins": [ + { + "name": "instavm-coderunner", + "source": ".", + "description": "Execute Python code in a local sandboxed Apple container with fast kernel pool performance" + } + ] +} diff --git a/instavm-coderunner-plugin/.claude-plugin/plugin.json b/instavm-coderunner-plugin/.claude-plugin/plugin.json new file mode 100644 index 0000000..c8494ad --- /dev/null +++ b/instavm-coderunner-plugin/.claude-plugin/plugin.json @@ -0,0 +1,10 @@ +{ + "name": "instavm-coderunner", + "version": "1.0.0", + "description": "Execute Python code in a sandboxed Apple container via local CodeRunner MCP server", + "author": { + "name": "InstaVM" + }, + "homepage": "https://github.com/instavm/coderunner", + "license": "MIT" +} diff --git a/instavm-coderunner-plugin/.mcp.json b/instavm-coderunner-plugin/.mcp.json new file mode 100644 index 0000000..e807d64 --- /dev/null +++ b/instavm-coderunner-plugin/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "instavm-coderunner": { + "command": "python3", + "args": ["${CLAUDE_PLUGIN_ROOT}/scripts/mcp-proxy.py"] + } + } +} diff --git a/instavm-coderunner-plugin/README.md b/instavm-coderunner-plugin/README.md new file mode 100644 index 0000000..0127935 --- /dev/null +++ b/instavm-coderunner-plugin/README.md @@ -0,0 +1,258 @@ +# InstaVM CodeRunner Plugin for Claude Code + +This plugin enables Claude Code to execute Python code in a local sandboxed Apple container using [InstaVM CodeRunner](https://github.com/instavm/coderunner) via the Model Context Protocol (MCP). + +## Quick Start + +```bash +# 1. Install and start CodeRunner (one-time setup) +git clone https://github.com/instavm/coderunner.git +cd coderunner +sudo ./install.sh + +# 2. Install the Claude Code plugin +claude plugin marketplace add github:BandarLabs/coderunner/instavm-coderunner-plugin +claude plugin install instavm-coderunner@instavm-plugins + +# 3. Reconnect to MCP servers +/mcp +``` + +That's it! Claude Code now has access to all CodeRunner tools. + +## Installation + +### Prerequisites + +**IMPORTANT:** You must have CodeRunner installed and running **before** installing this plugin. + +#### Install CodeRunner + +```bash +git clone https://github.com/instavm/coderunner.git +cd coderunner +sudo ./install.sh +``` + +This will: +- Install Apple's container runtime (version 0.8.0+) +- Pull the `instavm/coderunner` container image +- Start the CodeRunner MCP service at `http://coderunner.local:8222/mcp` + +**Verify CodeRunner is running:** +```bash +curl http://coderunner.local:8222/execute -X POST -H "Content-Type: application/json" -d '{"code":"print(\"test\")"}' +``` + +### Install via GitHub URL (Recommended) + +```bash +# Add the InstaVM marketplace from GitHub +claude plugin marketplace add github:BandarLabs/coderunner/instavm-coderunner-plugin + +# Install the plugin +claude plugin install instavm-coderunner@instavm-plugins +``` + +### Option 2: Install from Local Path + +If you've cloned the repository locally: + +```bash +# Add local marketplace +claude plugin marketplace add /path/to/coderunner/instavm-coderunner-plugin + +# Install the plugin +claude plugin install instavm-coderunner@instavm-plugins +``` + +## Usage + +Once installed, Claude will have access to the `execute_python_code` tool from CodeRunner. You can ask Claude to execute Python code: + +``` +Please execute this Python code: +```python +import math +print(f"The square root of 16 is {math.sqrt(16)}") +``` +``` + +Or simply: +``` +Execute this code: print("Hello from CodeRunner!") +``` + +## Available Tools + +The plugin exposes the following MCP tools from CodeRunner: + +- **execute_python_code**: Execute Python code in a persistent Jupyter kernel with full stdout/stderr capture +- **navigate_and_get_all_visible_text**: Web scraping using Playwright - navigate to URLs and extract visible text +- **list_skills**: List all available skills (both public and user-added) in CodeRunner +- **get_skill_info**: Get documentation for a specific skill (reads SKILL.md) +- **get_skill_file**: Read any file from a skill's directory (e.g., EXAMPLES.md, API.md) + +## Features + +- **Local Execution**: Code runs on your machine in a sandboxed container +- **No Cloud Uploads**: Process files locally without uploading to cloud services +- **Fast Performance**: Pre-warmed Jupyter kernel pool (2-5 kernels) for quick execution +- **Full Output**: Returns stdout, stderr, execution time, and CPU time +- **Security**: Runs in Apple container isolation +- **MCP Integration**: Uses standard Model Context Protocol for tool communication + +## Configuration + +By default, the plugin connects to `http://coderunner.local:8222/mcp` via a stdio proxy. To customize the URL, set the `MCP_URL` environment variable in `.mcp.json`: + +```json +{ + "mcpServers": { + "instavm-coderunner": { + "command": "python3", + "args": ["${CLAUDE_PLUGIN_ROOT}/scripts/mcp-proxy.py"], + "env": { + "MCP_URL": "http://your-custom-url:8222/mcp" + } + } + } +} +``` + +## Example Output + +```python +# Code: +print("Hello from CodeRunner!") +import time +time.sleep(0.1) +for i in range(3): + print(f"Count: {i}") +``` + +**Result:** +``` +Hello from CodeRunner! +Count: 0 +Count: 1 +Count: 2 + +Execution time: 0.156s +``` + +## Requirements + +- macOS 26.0+ (recommended) for Apple Container support +- Python 3.10+ +- CodeRunner installed and running +- Claude Code with MCP plugin support + +## Troubleshooting + +### Plugin shows "failed" status + +**Most common cause:** CodeRunner container is not running. + +**Solution:** +```bash +# Check if CodeRunner is running +curl http://coderunner.local:8222/execute -X POST -H "Content-Type: application/json" -d '{"code":"print(\"test\")"}' + +# If not running, restart it: +cd /path/to/coderunner +sudo ./install.sh +``` + +### "Invalid Host header" error + +The CodeRunner MCP server is running but rejecting requests. This means the container needs to be restarted with proper hostname configuration. + +**Solution:** +```bash +# Stop existing containers +sudo pkill -f container +container system start + +# Restart CodeRunner container +container run --name coderunner --detach --rm --cpus 8 --memory 4g \ + --volume "$HOME/.coderunner/assets/skills/user:/app/uploads/skills/user" \ + --volume "$HOME/.coderunner/outputs:/app/uploads/outputs" \ + instavm/coderunner +``` + +### MCP connection errors + +1. **Check MCP logs:** + ```bash + cat ~/Library/Caches/claude-cli-nodejs/-Users-manish-coderunner-instavm-coderunner-plugin/mcp-logs-*/latest/*.jsonl + ``` + +2. **Test MCP endpoint manually:** + ```bash + curl -H "Host: coderunner.local:8222" http://coderunner.local:8222/mcp \ + -X POST -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05"}}' + ``` + +3. **Verify proxy script:** + ```bash + cd /path/to/instavm-coderunner-plugin + python3 scripts/mcp-proxy.py + # Then send: {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05"}} + ``` + +## How It Works + +This plugin uses Claude Code's MCP server integration to connect to the local CodeRunner instance: + +1. The `.mcp.json` file defines the MCP server connection +2. Python proxy script bridges stdio MCP to HTTP endpoint +3. Tools from CodeRunner are automatically discovered and made available +4. When you ask Claude to execute code, it uses the `execute_python_code` tool +5. Results are returned via the MCP protocol + +## Publishing to GitHub Marketplace + +This plugin is published on GitHub and can be installed directly via the GitHub URL. Here's how to publish updates: + +### 1. Commit and Push Changes + +```bash +cd instavm-coderunner-plugin +git add . +git commit -m "Update plugin" +git push origin main +``` + +### 2. Users Install from GitHub + +Users can install the plugin directly from GitHub: + +```bash +# Add the InstaVM marketplace +claude plugin marketplace add github:BandarLabs/coderunner/instavm-coderunner-plugin + +# Install the plugin +claude plugin install instavm-coderunner@instavm-plugins +``` + +That's it! Claude Code will automatically pull the plugin files from GitHub. + +## Repository Structure + +``` +coderunner/ +└── instavm-coderunner-plugin/ # Plugin directory + ├── .claude-plugin/ + │ ├── marketplace.json # Marketplace metadata + │ └── plugin.json # Plugin manifest + ├── scripts/ + │ └── mcp-proxy.py # stdio-to-HTTP MCP proxy + ├── .mcp.json # MCP server configuration + └── README.md # This file +``` + +## License + +MIT diff --git a/instavm-coderunner-plugin/install.sh b/instavm-coderunner-plugin/install.sh new file mode 100755 index 0000000..5dbb4f2 --- /dev/null +++ b/instavm-coderunner-plugin/install.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# InstaVM CodeRunner Plugin Installer for Claude Code + +PLUGIN_NAME="instavm-coderunner" +PLUGIN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "🚀 Installing $PLUGIN_NAME plugin for Claude Code..." +echo "" + +# Check if CodeRunner is running +echo "🔍 Checking if CodeRunner is running..." +if curl -s http://coderunner.local:8222/health > /dev/null 2>&1; then + echo "✅ CodeRunner is running at http://coderunner.local:8222" +else + echo "⚠️ Warning: CodeRunner does not appear to be running." + echo " Please install and start CodeRunner first:" + echo " cd /path/to/coderunner && sudo ./install.sh" + echo "" + read -p "Continue anyway? (y/N) " -n 1 -r + echo "" + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi +fi + +# Check MCP endpoint +echo "🔍 Checking MCP endpoint..." +if curl -s http://coderunner.local:8222/mcp > /dev/null 2>&1; then + echo "✅ MCP endpoint is accessible" +else + echo "⚠️ Warning: MCP endpoint not accessible" +fi + +echo "" +echo "📦 Plugin location: $PLUGIN_DIR" +echo "" +echo "To use this plugin with Claude Code, run:" +echo "" +echo " cd $PLUGIN_DIR" +echo " claude --plugin-dir ." +echo "" +echo "Or add it permanently:" +echo "" +echo " claude add plugin $PLUGIN_DIR" +echo "" +echo "✅ Setup complete!" diff --git a/instavm-coderunner-plugin/scripts/mcp-proxy.py b/instavm-coderunner-plugin/scripts/mcp-proxy.py new file mode 100755 index 0000000..3823d24 --- /dev/null +++ b/instavm-coderunner-plugin/scripts/mcp-proxy.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +""" +InstaVM CodeRunner MCP Proxy +Bridges stdio MCP protocol to HTTP endpoint with FastMCP session management +""" + +import sys +import os +import json +import urllib.request +import urllib.error +from urllib.parse import urlparse, urlencode + +# Get MCP URL from environment or use default +MCP_URL = os.environ.get("MCP_URL", "http://coderunner.local:8222/mcp") + +# Session management +session_id = None + +def send_request(request): + """Forward request to HTTP MCP server with session ID""" + global session_id + + try: + data = json.dumps(request).encode('utf-8') + parsed_url = urlparse(MCP_URL) + host_header = f"{parsed_url.hostname}:{parsed_url.port}" if parsed_url.port else parsed_url.hostname + + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json, text/event-stream', + 'Host': host_header + } + + # Add session ID header if we have one (but not for initialize) + if session_id and request.get("method") != "initialize": + headers['mcp-session-id'] = session_id + + url = MCP_URL + + req = urllib.request.Request( + url, + data=data, + headers=headers, + method='POST' + ) + + with urllib.request.urlopen(req, timeout=120) as response: + # Extract session ID from initialize response + if request.get("method") == "initialize": + session_id = response.headers.get('mcp-session-id') + if not session_id: + # Fallback: try Set-Cookie header + set_cookie = response.headers.get('Set-Cookie', '') + if 'mcp_session_id=' in set_cookie: + cookie_parts = set_cookie.split(';') + for part in cookie_parts: + if 'mcp_session_id=' in part: + session_id = part.split('=')[1].strip() + break + + # Handle SSE response format + body = response.read().decode('utf-8') + for line in body.split('\n'): + line = line.strip() + if line.startswith('data: '): + data_str = line[6:] # Remove 'data: ' prefix + if data_str and data_str != '[DONE]': + try: + response_obj = json.loads(data_str) + print(json.dumps(response_obj), flush=True) + except json.JSONDecodeError: + pass + + except urllib.error.HTTPError as e: + error_body = "" + try: + error_body = e.read().decode('utf-8') + except: + pass + + print(json.dumps({ + "jsonrpc": "2.0", + "id": request.get("id"), + "error": { + "code": e.code, + "message": f"HTTP Error: {e.reason}" + } + }), flush=True) + + except urllib.error.URLError as e: + print(json.dumps({ + "jsonrpc": "2.0", + "id": request.get("id"), + "error": { + "code": -32603, + "message": f"Connection Error: {e.reason}. Is CodeRunner running?" + } + }), flush=True) + + except Exception as e: + print(json.dumps({ + "jsonrpc": "2.0", + "id": request.get("id"), + "error": { + "code": -32603, + "message": f"Internal Error: {str(e)}" + } + }), flush=True) + +def main(): + """Main proxy loop""" + # Don't log on startup to avoid confusing MCP clients + for line in sys.stdin: + line = line.strip() + if not line: + continue + + try: + request = json.loads(line) + send_request(request) + except json.JSONDecodeError: + print(json.dumps({ + "jsonrpc": "2.0", + "id": None, + "error": { + "code": -32700, + "message": "Parse error" + } + }), flush=True) + +if __name__ == "__main__": + main() diff --git a/server.py b/server.py index 0691189..828d2f9 100644 --- a/server.py +++ b/server.py @@ -19,6 +19,7 @@ import httpx # Import Context for progress reporting from mcp.server.fastmcp import FastMCP, Context +from mcp.server.transport_security import TransportSecuritySettings from playwright.async_api import async_playwright from bs4 import BeautifulSoup import socket @@ -31,7 +32,24 @@ logger = logging.getLogger(__name__) # Initialize the MCP server with a descriptive name for the toolset -mcp = FastMCP("CodeRunner") +# Configure DNS rebinding protection to allow coderunner.local +mcp = FastMCP( + "CodeRunner", + transport_security=TransportSecuritySettings( + enable_dns_rebinding_protection=True, + allowed_hosts=[ + "localhost:*", + "127.0.0.1:*", + "coderunner.local:*", + "0.0.0.0:*", + ], + allowed_origins=[ + "http://localhost:*", + "http://127.0.0.1:*", + "http://coderunner.local:*", + ], + ) +) # Kernel pool configuration MAX_KERNELS = 5 @@ -760,29 +778,31 @@ async def api_execute(request: Request): "timeout": 300 // optional, not used in local execution } - Response (JSON): - { - "output": "hello world\\n", - "status": "success" - } - or + Response (JSON) - matches api.instavm.io/execute format: { - "output": "", - "error": "error message", - "status": "error" + "stdout": "hello world\\n", + "stderr": "", + "execution_time": 0.39, + "cpu_time": 0.03 } """ + import time + start_time = time.time() + try: # Parse request body body = await request.json() - command = body.get("command") + + # SDK sends "code" field, direct API calls use "command" + command = body.get("code") or body.get("command") if not command: - return { - "output": "", - "error": "Missing 'command' field in request body", - "status": "error" - } + return JSONResponse({ + "stdout": "", + "stderr": "Missing 'code' or 'command' field in request body", + "execution_time": 0.0, + "cpu_time": 0.0 + }, status_code=400) # Create mock context for progress reporting ctx = MockContext() @@ -790,26 +810,36 @@ async def api_execute(request: Request): # Execute the code result = await execute_python_code(command, ctx) + # Calculate execution time + execution_time = time.time() - start_time + # Check if result contains an error if result.startswith("Error:"): return JSONResponse({ - "output": "", - "error": result, - "status": "error" + "stdout": "", + "stderr": result, + "execution_time": execution_time, + "cpu_time": execution_time # Approximate CPU time as execution time }) + # For compatibility with api.instavm.io, return stdout/stderr format + # Since execute_python_code returns combined output, we put it all in stdout return JSONResponse({ - "output": result, - "status": "success" + "stdout": result, + "stderr": "", + "execution_time": execution_time, + "cpu_time": execution_time # Approximate CPU time as execution time }) except Exception as e: logger.error(f"Error in /execute endpoint: {e}", exc_info=True) + execution_time = time.time() - start_time return JSONResponse({ - "output": "", - "error": f"Error: {str(e)}", - "status": "error" - }) + "stdout": "", + "stderr": f"Error: {str(e)}", + "execution_time": execution_time, + "cpu_time": execution_time + }, status_code=500) async def api_browser_navigate(request: Request): @@ -925,7 +955,74 @@ async def api_browser_extract_content(request: Request): "error": f"Error: {str(e)}" }) +# --- SESSION MANAGEMENT ENDPOINTS FOR SDK COMPATIBILITY --- + +# Simple in-memory session store (for local use, sessions are lightweight) +_session_store = {} +_session_counter = 0 + + +async def api_start_session(request: Request): + """ + Start a new session (compatible with InstaVM SDK). + + For local execution, sessions are lightweight - we just return a session ID. + The SDK uses this for tracking, but locally we don't need complex session state. + + Response (JSON): + { + "session_id": "session_123", + "status": "active" + } + """ + global _session_counter + _session_counter += 1 + session_id = f"session_{_session_counter}" + + # Store session (minimal state for local use) + _session_store[session_id] = { + "status": "active", + "created_at": __import__('time').time() + } + + return JSONResponse({ + "session_id": session_id, + "status": "active" + }) + + +async def api_get_session(request: Request): + """ + Get session status (compatible with InstaVM SDK). + + Response (JSON): + { + "session_id": "session_123", + "status": "active" + } + """ + # For local use, just return active status + return JSONResponse({ + "session_id": "session", + "status": "active" + }) + + +async def api_stop_session(request: Request): + """ + Stop a session (compatible with InstaVM SDK). + + For local use, this is a no-op since we don't have real session state. + """ + return JSONResponse({ + "status": "stopped" + }) + + # Add routes to the Starlette app app.add_route("/execute", api_execute, methods=["POST"]) +app.add_route("/v1/sessions/session", api_start_session, methods=["POST"]) +app.add_route("/v1/sessions/session", api_get_session, methods=["GET"]) +app.add_route("/v1/sessions/session", api_stop_session, methods=["DELETE"]) app.add_route("/v1/browser/interactions/navigate", api_browser_navigate, methods=["POST"]) app.add_route("/v1/browser/interactions/content", api_browser_extract_content, methods=["POST"]) \ No newline at end of file