User Guide
December 5, 2025 · View on GitHub
A comprehensive guide to using the MCP Server Code Execution Mode bridge.
Table of Contents
- Installation
- Configuration
- MCP Server Setup
- Usage Patterns
- Advanced Topics
- Troubleshooting
- Best Practices
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 separatetypingpackage 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:
- 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)
-
MCP Servers Directory
~/.config/mcp/servers/*.json./mcp-servers/*.json
-
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:
cwdis 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. Settingcwdensures the server runs in the directory you expect. - How LLMs should discover it: Agents should call
runtime.describe_server(name)or inspectruntime.list_loaded_server_metadata()to find acwdentry 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
cwdis 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 ifcwdis missing. - If the server doesn't accept
cwdin its JSON: Older or third-party MCP servers may not have acwdfield in their config. This is fine —cwdis optional. If your workflow needs a specific directory, configure it on the host (or usedocker run/podman runin 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/stderrexactly as emitted while trimming empty fields fromstructuredContent. 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=toonwhen you prefer Token-Oriented Object Notation blocks. We still drop empty strings/collections before encoding, and the TOON block mirrors the samestructuredContentpayload. - 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
SANDBOX_HELPERS_SUMMARYonly 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.- Typical agent interactions begin with
await mcp.runtime.discovered_servers()(orruntime.list_servers_sync()when you just need the cached list) to see which MCP servers are available for the current run. - The agent then fetches documentation on demand via
await mcp.runtime.query_tool_docs(server)or performs fuzzy lookups withawait mcp.runtime.search_tool_docs("keyword"). - Armed with those results, the agent calls the auto-generated
mcp_<alias>proxies orawait mcp.runtime.call_tool(...)inside its Python code. - 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
- Session Continuity: Save conversation context, decisions, and progress
- Learning: Store successful patterns and past solutions
- Configuration: Persist user preferences and project settings
- State Machines: Track workflow progress across multiple calls
- Caching: Store expensive computation results
Memory vs. User Tools
| Feature | Memory | User Tools (save_tool) |
|---|---|---|
| Storage | JSON data | Python functions |
| Purpose | State, context, data | Reusable code |
| Access | load_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:
- Install podman or docker
- Verify:
podman --version - 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:
- Authenticate the Docker daemon with every registry referenced in the gateway catalog (run
docker loginfor Docker Hub andghcr.ioas needed). - Ensure required secrets (for example
github.personal_access_tokenforgithub-official) are set viadocker mcp secret set <name>or your gateway's secrets backend. - Replicate any expected environment variables or volume mounts defined in the catalog so each server can find its configuration data.
- Re-run the bridge and inspect the gateway logs; if
list_toolsonly returnsmcp-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:
- Verify server in config:
~/.config/mcp/servers/*.json - Check bridge logs for discovery messages
- Restart bridge after adding server
- 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:
- Keep podman machine running (avoid shutdown)
- Use local image:
podman pull python:3.13-slim - Consider caching strategies
- 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:
- Check STATUS.md for roadmap
- Review ARCHITECTURE.md for technical details
- See HISTORY.md for evolution