diff --git a/packages/python/README.md b/packages/python/README.md index 4bcc5a9..cd42064 100644 --- a/packages/python/README.md +++ b/packages/python/README.md @@ -4,9 +4,19 @@ A Model Context Protocol server for running code in a secure sandbox by [E2B](ht ## Development -Install dependencies: +Install dependencies with Poetry: ``` -uv install +cd packages/python +poetry install +``` + +Or install directly with pip (from project root): +``` +python3 -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip +cd packages/python +python -m pip install --force-reinstall . ``` ## Installation @@ -30,14 +40,58 @@ On Windows: `%APPDATA%/Claude/claude_desktop_config.json` ### Debugging -Since MCP servers communicate over stdio, debugging can be challenging. We recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), which is available as a package script: +Since MCP servers communicate over stdio, debugging can be challenging. We recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector). You can point it at the Poetry command directly, for example: ``` -npx @modelcontextprotocol/inspector \ - uv \ - --directory . \ - run \ - e2b-mcp-server \ +cd packages/python +npx @modelcontextprotocol/inspector -- poetry run e2b-mcp-server ``` The Inspector will provide a URL to access debugging tools in your browser. + +## HTTP Server (Official SDK - Stateless) + +Uses the official MCP Python SDK's stateless HTTP transport (no session persistence, no SSE stream). + +### Run (HTTP) + +With Poetry: +``` +cd packages/python +poetry run e2b-mcp-server-http +``` + +With pip (after following development setup above): +``` +e2b-mcp-server-http +``` + +### Authentication + +The server requires an `Authorization: Bearer ` header for all requests. The token is passed through to the E2B SDK for sandbox operations. + +### Examples + +The server endpoints and behavior are managed by the official MCP SDK. Check the server logs for the actual URL when it starts. + +List tools: + +``` +curl -s \ + -H "Authorization: Bearer YOUR_E2B_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \ + http://localhost:8000/mcp +``` + +Call tool: + +``` +curl -s \ + -H "Authorization: Bearer YOUR_E2B_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"run_code","arguments":{"code":"print(\"hello world\")"}}}' \ + http://localhost:8000/mcp +``` + +The server runs in stateless mode with regular JSON responses (no SSE streaming). The actual host, port, and mount path are determined by the SDK's HTTP transport configuration. diff --git a/packages/python/e2b_mcp_server/http_server.py b/packages/python/e2b_mcp_server/http_server.py new file mode 100644 index 0000000..13cc6e4 --- /dev/null +++ b/packages/python/e2b_mcp_server/http_server.py @@ -0,0 +1,115 @@ +""" +Run the E2B MCP server with streamable HTTP transport using the official SDK. +""" + +import contextvars +import json +import logging +from collections.abc import Sequence + +from dotenv import load_dotenv +from mcp.server.fastmcp import FastMCP +from mcp.server.auth.provider import AccessToken, TokenVerifier +from mcp.server.auth.settings import AuthSettings +from mcp.types import TextContent, ImageContent, EmbeddedResource, Tool +from e2b_code_interpreter import Sandbox +from pydantic import BaseModel, AnyHttpUrl + +# Load environment variables +load_dotenv() + +# Suppress known harmless errors from MCP SDK streamable HTTP transport +logging.getLogger("mcp.server.streamable_http").setLevel(logging.CRITICAL) + +# Store the token for the current request +current_token: contextvars.ContextVar[str | None] = contextvars.ContextVar("current_token", default=None) + + +# Tool schema (copied from server.py for HTTP version) +class ToolSchema(BaseModel): + code: str + + +class E2BTokenVerifier(TokenVerifier): + """Token verifier that stores the E2B API key for use in tools.""" + + async def verify_token(self, token: str) -> AccessToken | None: + """Verify token and store it for the current request.""" + # Store the token in context for use in tools + current_token.set(token) + + # Return a valid access token - we don't validate, just pass through + return AccessToken( + token=token, + scopes=["e2b"], # Dummy scope + client_id="e2b-mcp-client", # Required field + ) + + +# Create FastMCP server instance with token verifier and auth settings +mcp = FastMCP( + "e2b-mcp-server", + stateless_http=True, + json_response=True, + token_verifier=E2BTokenVerifier(), + auth=AuthSettings( + issuer_url=AnyHttpUrl("https://e2b.dev"), # E2B as issuer + resource_server_url=AnyHttpUrl("http://localhost:8000"), # This server's URL + required_scopes=["e2b"], + ), +) + +def tool_response(exec_obj): + """Convert Execution object to a serializable dict""" + result = {} + + # Logs + if hasattr(exec_obj, "logs"): + result["stdout"] = exec_obj.logs.stdout if hasattr(exec_obj.logs, "stdout") else [] + result["stderr"] = exec_obj.logs.stderr if hasattr(exec_obj.logs, "stderr") else [] + + # Results (if any) + if hasattr(exec_obj, "results"): + result["results"] = exec_obj.results + + # Error + if hasattr(exec_obj, "error") and exec_obj.error: + err = exec_obj.error + result["error"] = { + "name": getattr(err, "name", None), + "value": getattr(err, "value", None), + "traceback": getattr(err, "traceback", None) + } + + return result + +@mcp.tool() +async def run_code(code: str) -> Sequence[TextContent | ImageContent | EmbeddedResource]: + """Run python code in a secure sandbox by E2B. Using the Jupyter Notebook syntax.""" + # Get the token from the current request context - mandatory for HTTP mode + api_key = current_token.get() + if not api_key: + raise ValueError("Authorization header with Bearer token is required") + + sandbox = Sandbox(api_key=api_key) + execution = sandbox.run_code(code) + + result = tool_response(execution) + + return [ + TextContent( + type="text", + text=json.dumps(result, indent=2) + ) + ] + + +def main() -> None: + """Run server with streamable HTTP transport.""" + mcp.run(transport="streamable-http") + + +if __name__ == "__main__": + main() + + diff --git a/packages/python/pyproject.toml b/packages/python/pyproject.toml index 73af971..29a7c3d 100644 --- a/packages/python/pyproject.toml +++ b/packages/python/pyproject.toml @@ -9,11 +9,15 @@ homepage = "https://e2b.dev/" repository = "https://github.com/e2b-dev/mcp-server/tree/main/packages/python" packages = [{ include = "e2b_mcp_server" }] +[tool.poetry.scripts] +e2b-mcp-server = "e2b_mcp_server:main" +e2b-mcp-server-http = "e2b_mcp_server.http_server:main" + [tool.poetry.dependencies] python = ">=3.10,<4.0" e2b-code-interpreter = "^1.0.2" -mcp = "^1.0.0" +mcp = ">=1.12.0" pydantic = "^2.10.2" python-dotenv = "1.0.1"