User Guide

December 5, 2025 · View on GitHub

A comprehensive guide to using the MCP Server Code Execution Mode bridge.

Table of Contents

Installation

Prerequisites

1. Container Runtime

Option A: Podman (Recommended)

# macOS
brew install podman

# Ubuntu/Debian
sudo apt-get install podman

# Verify
podman --version

Option B: Rootless Docker

# macOS
brew install docker

# Ubuntu/Debian
sudo apt-get install docker.io

# Add user to docker group
sudo usermod -aG docker $USER
newgrp docker

2. Container Image

# Pull image
podman pull python:3.13-slim

# Or with Docker
docker pull python:3.13-slim

# Verify
podman images python:3.13-slim

Note on Pydantic compatibility:

  • If you use Python 3.14+, ensure you have a modern Pydantic release installed (for example, pydantic >= 2.12.0). Some older Pydantic versions or environments that install a separate typing package from PyPI may raise errors such as:
TypeError: _eval_type() got an unexpected keyword argument 'prefer_fwd_module'

If you see this error, run:

pip install -U pydantic
pip uninstall typing  # if present; the stdlib's typing should be used

And re-run uv sync.

Setup

1. Install Dependencies

# Using pip
pip install -r requirements.txt

# Or using uv (recommended)
uv sync

2. Test Installation

uv run python mcp_server_code_execution_mode.py

This starts the MCP server. If no errors occur, the installation is successful.

3. Register with MCP Client

Claude Code & OpenCode:

Create ~/.config/mcp/servers/mcp-server-code-execution-mode.json or place equivalent configuration under OpenCode's config file (e.g. ~/.opencode.json):

{
  "mcpServers": {
    "mcp-server-code-execution-mode": {
      "command": "uvx",
      "args": [
        "--from",
        "git+https://github.com/elusznik/mcp-server-code-execution-mode",
        "mcp-server-code-execution-mode",
        "run"
      ],
      "env": {
        "MCP_BRIDGE_RUNTIME": "podman"
      }
    }
  }
}

For other MCP clients:

Add server to your client configuration:

{
  "mcpServers": {
    "mcp-server-code-execution-mode": {
      "command": "python3",
      "args": ["/path/to/mcp_server_code_execution_mode.py"]
    }
  }
}

4. Restart MCP Client

Restart Claude Code or your MCP client to load the new server.

Configuration

Environment Variables

Control bridge behavior with environment variables:

Runtime Configuration

# Force specific runtime
export MCP_BRIDGE_RUNTIME=podman
# or
export MCP_BRIDGE_RUNTIME=docker

# Custom container image
export MCP_BRIDGE_IMAGE=python:3.11-slim

# Default timeout (seconds)
export MCP_BRIDGE_TIMEOUT=30

# Maximum allowed timeout
export MCP_BRIDGE_MAX_TIMEOUT=120

Resource Limits

# Memory limit (format: number + unit)
export MCP_BRIDGE_MEMORY=512m
export MCP_BRIDGE_MEMORY=1g

# Process limit
export MCP_BRIDGE_PIDS=128

# CPU limit (can be decimal)
export MCP_BRIDGE_CPUS=2.0

# Container user (UID:GID)
export MCP_BRIDGE_CONTAINER_USER=1000:1000

Advanced Options

# Runtime idle timeout (seconds)
# Podman machine auto-shutdown delay
export MCP_BRIDGE_RUNTIME_IDLE_TIMEOUT=300

Output Formatting

# Default responses are compact plain text.
# Set to 'toon' when you want rich TOON blocks instead.
export MCP_BRIDGE_OUTPUT_MODE=toon

# Reduce bridge log noise (defaults to INFO)
export MCP_BRIDGE_LOG_LEVEL=WARNING

Configuration File

Note: The bridge currently does not support loading variables from a .env file. All configuration must be done via environment variables or container runtime settings.

MCP Server Setup

Automatic Discovery

The bridge auto-discovers MCP servers from:

  1. Claude Code Config
  • ~/.claude.json
  • ~/Library/Application Support/Claude Code/claude_code_config.json
  • ~/Library/Application Support/Claude/claude_code_config.json (early Claude Code builds)
  • ~/Library/Application Support/Claude/claude_desktop_config.json (legacy Claude Desktop)
  1. MCP Servers Directory

    • ~/.config/mcp/servers/*.json
    • ./mcp-servers/*.json
  2. Local Config

  • ./claude_code_config.json
  • ./claude_desktop_config.json (legacy fallback)

1b. OpenCode Config - ~/.opencode.json - ~/Library/Application Support/OpenCode/opencode_config.json - ~/Library/Application Support/OpenCode/opencode_desktop_config.json (legacy fallback) - ./opencode_config.json - ./opencode_desktop_config.json (legacy fallback)

Example: Filesystem Server

# Create server config
mkdir -p ~/.config/mcp/servers
cat > ~/.config/mcp/servers/filesystem.json << 'EOF'
{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
      "env": {}
    }
  }
}
EOF

Example: PostgreSQL Server

cat > ~/.config/mcp/servers/postgres.json << 'EOF'
{
  "mcpServers": {
    "postgres": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-postgres", "postgresql://user:pass@localhost/mydb"],
      "env": {}
    }
  }
}
EOF

Example: Git Server

cat > ~/.config/mcp/servers/git.json << 'EOF'
{
  "mcpServers": {
    "git": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-git", "/path/to/repo"],
      "cwd": "/home/user/projects/repo",
      "env": {}
    }
  }
}
EOF

Verifying Discovery

The bridge logs discovered servers on startup:

2024-01-01 12:00:00 - INFO - Loaded MCP servers: filesystem, postgres, git

Server Working Directory (cwd)

  • What it is: cwd is an optional property of the server configuration that tells the bridge which working directory to use when spawning the host process for the MCP server.
  • Why it matters: Some servers (like uvx-backed servers or file-oriented servers) rely on the working directory to locate project files. Setting cwd ensures the server runs in the directory you expect.
  • How LLMs should discover it: Agents should call runtime.describe_server(name) or inspect runtime.list_loaded_server_metadata() to find a cwd entry in the returned metadata. If present, your code or the agent can assume the server's working directory.

Example: discover server's cwd in the sandbox

from mcp import runtime

desc = runtime.describe_server('serena')
cdir = desc.get('cwd') or 'bridge-default'
print('Server cwd:', cdir)
  • Fallback if missing: If cwd is not present, the host starts the process in the bridge's default working directory (typically where the bridge runs). Agents should avoid assuming a server's working directory if cwd is missing.
  • If the server doesn't accept cwd in its JSON: Older or third-party MCP servers may not have a cwd field in their config. This is fine — cwd is optional. If your workflow needs a specific directory, configure it on the host (or use docker run/podman run in the server command to mount the workspace explicitly).

Note: LLMs cannot set cwd via run_python's servers parameter; it is part of your server configuration on the host. If you need a server to run in a particular workspace for a given task, either set cwd in the server's host-side configuration or start the server in a container that mounts the workspace path explicitly.

Tip for operators: Add cwd to your server's configuration to avoid LLMs needing to guess a working directory.

Usage Patterns

Response Formats

  • Compact (default) – Responses surface as plain text, preserving stdout/stderr exactly as emitted while trimming empty fields from structuredContent. This keeps prompts lean without losing important context. Stdio mirroring is unchanged: everything your code prints still reaches the client.
  • TOON mode – Set MCP_BRIDGE_OUTPUT_MODE=toon when you prefer Token-Oriented Object Notation blocks. We still drop empty strings/collections before encoding, and the TOON block mirrors the same structuredContent payload.
  • JSON fallback – If the TOON encoder is missing the bridge automatically falls back to indented JSON blocks, so integrations always receive readable text alongside the structured data.

Tool Discovery Flow

  1. SANDBOX_HELPERS_SUMMARY only reminds the model that discovery helpers exist; it does not list servers or tools. The initial system prompt remains ~200 tokens even as catalogs grow.
  2. Typical agent interactions begin with await mcp.runtime.discovered_servers() (or runtime.list_servers_sync() when you just need the cached list) to see which MCP servers are available for the current run.
  3. The agent then fetches documentation on demand via await mcp.runtime.query_tool_docs(server) or performs fuzzy lookups with await mcp.runtime.search_tool_docs("keyword").
  4. Armed with those results, the agent calls the auto-generated mcp_<alias> proxies or await mcp.runtime.call_tool(...) inside its Python code.
  5. When the user simply asks “what can this MCP do?”, return runtime.capability_summary() instead of running exploratory code.

This discovery-first pattern keeps token usage nearly constant while still giving the LLM access to rich tool metadata whenever it needs it.

Basic Pattern: Direct Tool Use

# Call a single tool
result = await mcp_filesystem.read_file(path='/tmp/data.txt')
print(result)

Pattern: Chained Operations

# Chain multiple operations
data = await mcp_server.read_data()
processed = process(data)
await mcp_server.write_data(data=processed)

Pattern: Data Pipeline

# Extract
source_data = await mcp_source.fetch()

# Transform
cleaned = clean_data(source_data)

# Load
await mcp_destination.save(data=cleaned)

# Report
print(f"Processed {len(cleaned)} items")

Pattern: Batch Processing

# Get list
items = await mcp_api.list_items()

# Process in parallel
tasks = [
    mcp_api.process_item(id=item.id)
    for item in items
]

# Wait for all
results = await asyncio.gather(*tasks)

Pattern: Error Handling

try:
    result = await mcp_api.risky_operation()
except Exception as e:
    print(f"Operation failed: {e}")
    # Fallback or retry logic

Pattern: Conditional Execution

# Check before acting
status = await mcp_service.check_status()

if status.ready:
    await mcp_service.execute()
else:
    print("Service not ready")

Pattern: Multi-Server Workflow

# Get data from service A
data = await mcp_service_a.fetch_data(query='xyz')

# Process with service B
processed = await mcp_service_b.process(data=data)

# Save with service C
await mcp_service_c.save(data=processed)

# Notify with service D
await mcp_service_d.notify(message='Done')

Loading Servers for a Run

Only the MCP servers you request are available inside the sandbox. Include the servers array whenever you invoke run_python so proxies like mcp_serena are generated:

{
  "code": "print(await mcp_serena.search(query='latest AI papers'))",
  "servers": ["serena", "filesystem"]
}

Without that list the discovery helpers still enumerate the catalog, but RPC calls to unloaded servers return Server '<name>' is not available.

Pattern: Discover and Select Servers

from mcp import runtime

# See everything the bridge knows about without loading schemas
print("Discovered:", runtime.discovered_servers())
print("Cached servers:", runtime.list_servers_sync())

# Metadata for servers already loaded in this run
print("Loaded metadata:", runtime.list_loaded_server_metadata())

# Ask the host to enumerate every available server (RPC call)
available = await runtime.list_servers()
print("Selectable via RPC:", available)

# Peek at tool docs before deciding to use them
loaded = runtime.list_loaded_server_metadata()
if loaded:
  description = runtime.describe_server(loaded[0]["name"])
  for tool in description["tools"]:
    print(tool["alias"], "→", tool.get("description", ""))

# Summaries or full schemas only when needed
if loaded:
  summaries = await runtime.query_tool_docs(loaded[0]["name"])
  detailed = await runtime.query_tool_docs(
    loaded[0]["name"],
    tool=summaries[0]["toolAlias"],
    detail="full",
  )
  print("Summaries:", summaries)
  print("Detailed doc:", detailed)
  print("Cached tools:", runtime.list_tools_sync(loaded[0]["name"]))

# Keyword search across the servers already loaded in this run
results = await runtime.search_tool_docs("calendar events", limit=3)
for result in results:
  print(result["server"], result["tool"], result.get("description", ""))

# Quick answers without awaiting RPC
print("Capability summary:", runtime.capability_summary())
print("Cached docs:", runtime.query_tool_docs_sync(loaded[0]["name"]) if loaded else [])
print("Cached search:", runtime.search_tool_docs_sync("calendar"))

Typical output for the stub test server:

Discovered: ('stub',)
Loaded metadata: ({'name': 'stub', 'alias': 'stub', 'tools': [{'name': 'echo', 'alias': 'echo', 'description': 'Echo the provided message', 'input_schema': {...}}]},)
Selectable via RPC: ('stub',)

Advanced Topics

Persistent Memory System

The bridge provides a built-in memory system for persisting information across sessions. Memory is stored as JSON files in /projects/memory/ inside the container, which maps to ~/MCPs/user_tools/memory/ on the host.

Core Memory Functions

# Save any JSON-serializable value with optional metadata
save_memory("project_context", {
    "goal": "Build REST API",
    "current_task": "Implement auth",
    "decisions": ["Use JWT", "PostgreSQL"]
}, metadata={"tags": ["important"]})

# Load a value (returns default if not found)
context = load_memory("project_context")
context = load_memory("nonexistent", default={})

# Delete a memory entry
delete_memory("outdated_info")

# Check if a memory exists
if memory_exists("user_preferences"):
    prefs = load_memory("user_preferences")

Listing and Inspecting Memories

# List all saved memories
for mem in list_memories():
    print(f"{mem['key']}: created {mem['created_at']}")
    print(f"  metadata: {mem['metadata']}")

# Get full info about a specific memory (includes value)
info = get_memory_info("project_context")
print(f"Value: {info['value']}")
print(f"Created: {info['created_at']}, Updated: {info['updated_at']}")

Atomic Updates

Use update_memory for read-modify-write operations:

# Increment a counter
update_memory("call_count", lambda x: (x or 0) + 1)

# Append to a list
update_memory("task_log", lambda log: (log or []) + [{"task": "auth", "status": "done"}])

# Update nested data
update_memory("project_context", lambda ctx: {
    **(ctx or {}),
    "current_task": "Implement rate limiting",
    "decisions": (ctx or {}).get("decisions", []) + ["Add Redis cache"]
})

Memory Use Cases

  1. Session Continuity: Save conversation context, decisions, and progress
  2. Learning: Store successful patterns and past solutions
  3. Configuration: Persist user preferences and project settings
  4. State Machines: Track workflow progress across multiple calls
  5. Caching: Store expensive computation results

Memory vs. User Tools

FeatureMemoryUser Tools (save_tool)
StorageJSON dataPython functions
PurposeState, context, dataReusable code
Accessload_memory()import or call directly
Location/projects/memory//projects/user_tools.py

Both persist to the same host directory (~/MCPs/user_tools/) and survive container restarts.

Custom Timeout Per Call

# Set timeout for specific operation
result = await mcp_slow_service.long_operation(
    timeout=60  # Override default 30s
)

Loading Specific Servers

# Only load necessary servers
# When invoking run_python from your MCP client, specify the servers you need:
#   servers=['filesystem']
# Inside the sandboxed code you simply call the proxy:
result = await mcp_filesystem.read_file(path='/tmp/test.txt')

Accessing Raw MCP Client

# Direct server access
server = mcp_servers['filesystem']
result = await server.read_file(path='/tmp')

Loading Specific Servers

Note: The servers parameter is only used when making the initial MCP tool call. The sandbox code sees only the proxies that were requested up front.

Troubleshooting

Startup throws TypeError: 'async for' requires an object with __aiter__

Problem:

TypeError: 'async for' requires an object with __aiter__ method, got Server

Solution: You are likely running a pre-0.2.1 build that passed the server instance into stdio_server. Upgrade to the latest release (or reinstall via uvx --from git+https://github.com/elusznik/mcp-server-code-execution-mode mcp-server-code-execution-mode run) and retry.

Container Runtime Not Found

Problem:

Error: No container runtime found

Solution:

  1. Install podman or docker
  2. Verify: podman --version
  3. Set explicit runtime: export MCP_BRIDGE_RUNTIME=podman

Image Pull Failed

Problem:

Error: Failed to pull image python:3.13-slim

Solution:

# Manually pull
podman pull python:3.13-slim

# Or use different image
export MCP_BRIDGE_IMAGE=python:3.13-slim

Gateway Servers Fail to Initialize

Problem:

failed to connect: calling "initialize": EOF

Solution:

  1. Authenticate the Docker daemon with every registry referenced in the gateway catalog (run docker login for Docker Hub and ghcr.io as needed).
  2. Ensure required secrets (for example github.personal_access_token for github-official) are set via docker mcp secret set <name> or your gateway's secrets backend.
  3. Replicate any expected environment variables or volume mounts defined in the catalog so each server can find its configuration data.
  4. Re-run the bridge and inspect the gateway logs; if list_tools only returns mcp-add/code-mode, the external servers still are not starting.

Permission Denied

Problem:

Error: permission denied while trying to connect

Solution:

# Add user to docker group
sudo usermod -aG docker $USER
newgrp docker

# Or use podman (user namespaces)
podman info  # Verify user namespace

Timeout Errors

Problem:

SandboxTimeout: Code exceeded timeout

Solution:

# Increase timeout
export MCP_BRIDGE_TIMEOUT=60

# Or per-call
result = await mcp_operation(timeout=60)

Server Not Found

Problem:

Error: MCP server 'xyz' is not loaded

Solution:

  1. Verify server in config: ~/.config/mcp/servers/*.json
  2. Check bridge logs for discovery messages
  3. Restart bridge after adding server
  4. Explicitly request server: servers=['xyz']

Network Issues (In Container)

Problem:

Error: Network is unreachable

Expected: Containers have no network access by design.

Solution: Access resources via MCP servers only.

Out of Memory

Problem:

Error: Memory limit exceeded

Solution:

# Increase memory limit
export MCP_BRIDGE_MEMORY=1g

# Or optimize code
# - Process data in chunks
# - Use generators
# - Clear references

Too Many Processes

Problem:

Error: Cannot fork: Resource temporarily unavailable

Solution:

# Increase PID limit
export MCP_BRIDGE_PIDS=256

# Or reduce process count in code

Slow Performance

Problem: Container startup is slow

Solutions:

  1. Keep podman machine running (avoid shutdown)
  2. Use local image: podman pull python:3.13-slim
  3. Consider caching strategies
  4. Reuse containers (not currently supported)

Best Practices

1. Resource Management

# GOOD: Process data in memory
data = await mcp_api.get_data()
processed = [item.transform() for item in data]

# BAD: Write large files to disk
await mcp_fs.write_file(path='/tmp/big.txt', data=huge_data)

2. Error Handling

# GOOD: Handle errors gracefully
try:
    result = await mcp_api.operation()
except Exception as e:
    logger.error(f"Operation failed: {e}")
    return None

# BAD: No error handling
result = await mcp_api.operation()

3. Batching

# GOOD: Batch requests
results = await asyncio.gather(
    mcp_api.call1(),
    mcp_api.call2(),
    mcp_api.call3()
)

# BAD: Sequential calls
r1 = await mcp_api.call1()
r2 = await mcp_api.call2()
r3 = await mcp_api.call3()

4. Timeouts

# GOOD: Set appropriate timeouts
await mcp_fast_operation()              # Uses default 30s
await mcp_slow_operation(timeout=60)    # Explicit 60s

# BAD: Using default for all
await mcp_slow_operation()  # May timeout

5. Code Organization

# GOOD: Modular code
def process_user_data(user_id):
    data = await mcp_api.get_user(user_id)
    return transform_user_data(data)

# Extract, transform, load
data = extract()
processed = transform(data)
await load(processed)

# BAD: Monolithic code
result = await mcp_api.call1()
result2 = await mcp_api.call2(result)
result3 = await mcp_api.call3(result2)

6. Security

# GOOD: Use MCP servers for sensitive operations
await mcp_vault.get_secret('api_key')

# BAD: Hardcode secrets
API_KEY = "sk-1234567890abcdef"

7. Idempotency

# GOOD: Idempotent operations
await mcp_api.upsert_record(id='123', data=updated_data)

# BAD: Non-idempotent
await mcp_api.create_record(id='123', ...)
await mcp_api.create_record(id='123', ...)  # Duplicate

8. Logging

# GOOD: Log operations
logger.info(f"Processing {len(items)} items")
result = await mcp_api.batch_process(items)
logger.info(f"Completed: {result.count} processed")

# BAD: Silent operations
result = await mcp_api.batch_process(items)

9. Data Size

# GOOD: Work with reasonable chunks
for batch in chunk_large_list(large_list, size=100):
    await mcp_api.process_batch(batch)

# BAD: Process everything at once
await mcp_api.process_batch(huge_list)

10. Cleanup

# Container auto-cleans up, but:
# - Use temporary paths for files
# - Let context managers handle cleanup
# - Don't rely on persistent state

# Each execution is stateless

Examples

See README.md for more examples.

Support

For issues, questions, or contributions: