Options for making a CodeExecutionServer¶
CodeExecutionServer is the base class for all MCP code execution servers in Agora Workbench. This guide covers the different ways to configure and instantiate one.
New to Agora Workbench?¶
The Your First Server tutorial walks you through building a server with a real tool step-by-step before diving into the full reference below.
Minimal server¶
The simplest server needs only a ServerConfig with a name, description, environment type, and dependency specification:
import asyncio
from agora_workbench.code_execution import CodeExecutionServer, ServerConfig
from agora_workbench.code_execution.auth import create_noop_auth_config
config = ServerConfig(
name="myserver",
description="Execute Python code with numpy and pandas.",
type="uv",
dependency_file="numpy\npandas\n",
)
server = CodeExecutionServer(server_config=config, auth_config=create_noop_auth_config())
if __name__ == "__main__":
asyncio.run(server.run_http(host="127.0.0.1", port=8000))
This gives you an MCP server with an execute_myserver_code tool that runs Python in an isolated environment with numpy and pandas available.
No-op authentication is local-only
create_noop_auth_config() accepts requests without validating the caller.
Keep code-execution servers on loopback. Use production authentication for
remote access; a non-loopback bind in no-op mode is rejected unless an
explicit external-network-boundary acknowledgement is provided.
Environment types¶
The type field on ServerConfig controls how the Python environment is managed:
| Type | Dependency format | Best for |
|---|---|---|
"uv" |
requirements.txt content |
Fast installs, pure-Python packages |
"conda" |
environment.yml content |
Scientific packages with native deps (RDKit, GDAL, etc.) |
"pip" |
requirements.txt content |
Legacy environments or Docker-baked deps |
Environment model¶
CodeExecutionServer always runs code in an isolated kernel environment built from ServerConfig.dependency_file, regardless of type (uv, conda, or pip).
- For
type="uv"andtype="pip",dependency_filecontainsrequirements.txtcontent - For
type="conda",dependency_filecontainsenvironment.ymlcontent - For native/compiled dependencies in conda environments (for example
ngspice,gdal,netcdf4), useconda-forgerather than system package managers likeapt-get
UV environment completion marker¶
A uv environment is ready only when both of these exist in build_dir:
bin/python.env_build_complete
Agora Workbench writes .env_build_complete only after dependency installation
and every additional_commands entry succeeds. Its content is not an API; its
presence records that the complete configured build finished.
If bin/python exists without the marker, the directory is treated as a stale
or interrupted environment. With auto_build=True, Agora Workbench removes the
directory and rebuilds it. With auto_build=False, startup reports that the
environment is missing or incomplete.
For a prebuilt container or an externally prepared build_dir, run
CodeExecutionServer.warm() during the image build whenever possible. If the
environment must be assembled outside Workbench, create
.env_build_complete only after installing all dependencies and successfully
running the configured additional commands. Do not store unrelated persistent
files in build_dir, because an incomplete directory may be removed during
automatic recovery.
Heavy models load per session
Each execute_{name}_code session runs in its own kernel process, so
anything a tool loads — including a multi-gigabyte model — is loaded once
per session. N concurrent sessions means N copies in RAM, which readily
exhausts a modest host. If your environment includes a large model or other
expensive process-global state, load it once in a sidecar
and share it across sessions instead.
Example: conda environment¶
config = ServerConfig(
name="chemistry",
description="RDKit cheminformatics environment.",
type="conda",
dependency_file="""\
name: chemistry
channels:
- conda-forge
dependencies:
- python=3.11
- rdkit
- numpy
- pandas
""",
)
Minimal native-dependency example:
config = ServerConfig(
name="circuits",
description="Conda environment with compiled/native dependencies.",
type="conda",
dependency_file="""\
name: circuits
channels:
- conda-forge
dependencies:
- python=3.11
- ngspice
- gdal
""",
)
Adding domain tools¶
Pass a ToolRegistry to expose typed domain tools that are discoverable via search_{name}_tools and callable through code execution:
from agora_workbench.code_execution import CodeExecutionServer, ServerConfig, ToolRegistry
from agora_workbench.code_execution.auth import create_noop_auth_config
from my_domain.tools import MY_TOOLS # list of ToolDefinition objects
registry = ToolRegistry()
for tool in MY_TOOLS:
registry.register_tool(tool)
server = CodeExecutionServer(
server_config=config,
tool_registry=registry,
auth_config=create_noop_auth_config(),
)
See Tool pattern for how to define ToolDefinition objects.
Subclassing for custom behavior¶
Subclass CodeExecutionServer to add preprocessing, custom hooks, or environment setup:
class ChemistryServer(CodeExecutionServer):
"""Inject common imports before every code execution."""
def preprocess_code(self, code: str) -> str:
return "from rdkit import Chem\nimport numpy as np\n" + code
Configuration reference¶
ServerConfig has six groups of settings:
Identity¶
| Field | Description |
|---|---|
name |
Server/environment name (becomes part of the MCP tool name) |
description |
Capabilities description (shown to the agent in the tool description) |
server_description |
Optional server-level description (FastMCP instructions) |
entra_client_id |
Entra ID app registration client ID |
entra_tenant_id |
Azure AD tenant ID |
Environment¶
| Field | Description |
|---|---|
type |
"uv", "conda", or "pip" |
dependency_file |
Content of the dependency file |
auto_build |
Build environment on startup if missing (default: True) |
build_dir |
Custom build directory (default: ~/.cache/mcp-envs/{name}) |
additional_commands |
Extra shell commands after env setup |
Assets¶
| Field | Description |
|---|---|
assets |
List of AssetSpec objects provisioned into the env cache at startup (see Working with data) |
auto_provision |
Fetch the declared assets on startup (default: True) |
Sidecars¶
| Field | Description |
|---|---|
sidecars |
List of SidecarConfig objects — long-lived helper processes launched at startup and stopped on shutdown, used to load an expensive resource (e.g. a model) once and share it across all kernel sessions over loopback HTTP (see Sidecars) |
Execution¶
| Field | Description |
|---|---|
execution_mode |
"sync" (default), "async_only", or "adaptive" — controls whether execute_{name}_code blocks, always backgrounds, or auto-promotes long-running calls |
promotion_threshold_s |
Seconds before adaptive mode promotes to background (default: 60) |
max_timeout |
Maximum allowed timeout per execution (default: 600s) |
default_timeout |
Default timeout (default: 300s) |
output_truncation_threshold |
Max chars in stdout/stderr before truncation |
parallel_max_concurrency |
Max parallel executions (0 = unlimited) |
kernel_network_mode |
"inherit" (effective default) or "isolated"; isolated kernels cannot route to the server or external IP networks. When omitted with a custom SessionManager, its SessionConfig setting is preserved. |
Isolating kernels from the network¶
Set kernel_network_mode="isolated" to launch every Jupyter kernel in an empty
Linux network namespace:
config = ServerConfig(
name="offline-analysis",
description="Analyze operator-provided local data without network access.",
type="uv",
dependency_file="numpy\npandas\n",
kernel_network_mode="isolated",
)
Workbench switches the Jupyter control channels from TCP to Unix IPC sockets, so
the server can still execute code while the kernel and its subprocesses have no
route to the server network or external IP networks. The launcher also creates a
subordinate user namespace and enables no_new_privs; kernel code does not
receive namespace capabilities in the server's user namespace.
This option is Linux-only and requires the unshare and setpriv commands from
util-linux, the ip command from iproute2, and runtime permission to create
unprivileged user and network namespaces. Workbench fails the kernel launch
rather than falling back to the inherited network when the boundary is
unavailable.
Network-isolated kernels cannot use:
- public or private HTTP services;
- host TCP loopback services, including Workbench
SidecarConfigsidecars; - server-to-server transfers initiated inside the kernel;
- domain tools that open IP sockets.
Private loopback remains available within each kernel namespace, so a kernel and
its own subprocesses can communicate over 127.0.0.1. They cannot use that
address to reach services running in the Workbench server namespace.
Environment construction, asset provisioning, catalog materialization, and artifact publishing run in the server process and are not affected. Files materialized before execution remain available to the kernel.
Network boundary, not a complete process sandbox
This setting isolates IP networking only. It does not remove inherited
environment variables or assign a different operating-system user to the
kernel. Creating the user namespace drops supplementary group memberships,
so paths accessible only through a supplementary group may become
unreadable. no_new_privs also prevents setuid helpers from gaining
privileges.
All kernels still run with the same host UID by default. Network isolation therefore does not prevent one kernel from accessing another kernel's files or Unix IPC endpoints when host filesystem permissions allow it. Do not expose host-control or network-proxy Unix sockets (for example, a container runtime socket) to kernel code. Hostile multi-user workloads additionally require per-kernel OS identities or separate worker isolation.
Features¶
| Field | Description |
|---|---|
tool_search_backend |
"bm25" (default) or "azure_ai_search" |
Authentication¶
Pass an AuthConfig to control how requests are authenticated:
from agora_workbench.code_execution.auth import create_noop_auth_config
from agora_workbench.code_execution.auth.entra import create_entra_auth_config
# For local development (no auth):
server = CodeExecutionServer(server_config=config, auth_config=create_noop_auth_config())
# For production with Entra ID:
server = CodeExecutionServer(server_config=config, auth_config=create_entra_auth_config())
See Authentication options for the full auth guide.
Publishers (artifact output)¶
Configure publishers to allow the agent to publish artifacts (files, plots) from code execution:
from agora_workbench.code_execution.auth import create_noop_auth_config
from agora_workbench.data_lake.execution import BlobPublisher, LocalFilePublisher
publishers = [
LocalFilePublisher(base_dir="/tmp/artifacts"),
BlobPublisher(account_url="https://myaccount.blob.core.windows.net", container="outputs"),
]
server = CodeExecutionServer(
server_config=config,
auth_config=create_noop_auth_config(),
publishers=publishers,
)
Running the server¶
import asyncio
# HTTP mode (standard deployment)
asyncio.run(server.run_http(host="127.0.0.1", port=8000))
# Warm up environment without serving (useful in Docker builds)
asyncio.run(server.warm())
The server exposes:
/mcp— MCP endpoint (SSE/streamable HTTP)/health— health check/.well-known/oauth-protected-resource— RFC 9728 OAuth protected resource metadata (when auth is enabled)