Skip to content

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" and type="pip", dependency_file contains requirements.txt content
  • For type="conda", dependency_file contains environment.yml content
  • For native/compiled dependencies in conda environments (for example ngspice, gdal, netcdf4), use conda-forge rather than system package managers like apt-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 SidecarConfig sidecars;
  • 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)