requires: codemode
A code mode script runs in the MCP gateway's sandbox each time an agent calls execute. This page lists what the script can reach, what each call returns and which limits apply. The code mode page explains when and how to use it.
The execute tool
| Argument | Type | Required | Description |
|---|---|---|---|
code | string | Yes | The script, at most 256 KiB. |
timeout_ms | integer ≥ 1 | No | Wall-clock limit for this run in milliseconds. It can only lower the listener's execution_timeout, never raise it. |
Unknown arguments are rejected. execute is the only tool a code-mode gateway lists, and calling any other name returns JSON-RPC error -32002 (Tool '<name>' not found).
How a script runs
- Async body. The code is the body of an
asyncfunction, soawaitworks at the top level andreturnends the run. The returned value is serialized as JSON, andundefinedbecomesnull. - Strip-only TypeScript. Type annotations, interfaces and type aliases are removed without moving any line or column.
enum,namespacedeclarations and decorators are rejected with aParseError. - Engine. The engine is QuickJS. Standard ECMAScript built-ins such as
JSON,Promise,MapandMathare available. There is nofetch, nosetTimeout, no module loading, no filesystem, no network and no environment. - Fresh state. Each run starts from a fresh copy of the engine, and nothing carries over between runs.
- Shutdown. When the script returns, calls still in flight are cancelled. A script whose promise never settles while no calls are pending ends with an
ExecutionFailure.
Globals
tools
| Form | Behaviour |
|---|---|
tools["namespace.name"](args) | Calls the tool with that qualified name. args defaults to {}. Returns a promise of a call envelope. |
tools.namespace.name(args) | The same call, written as property access. |
The name is split at its first ., so a local tool name may contain dots but a namespace may not. tools cannot be enumerated or changed: Object.keys(tools) and assignments throw. To see what exists, use catalog.
catalog
Every catalog operation takes one object argument and resolves to a call envelope. Only tools the caller is authorized to use are counted, listed, searched or described.
| Operation | Parameters | data on success |
|---|---|---|
catalog.namespaces.list | limit?, offset? | Page of { namespace, toolCount }, sorted by namespace. |
catalog.tools.search | query, namespace?, limit? | { items }: an array of { namespace, name, qualifiedName, description }, best match first. |
catalog.tools.list | namespace, limit?, offset? | Page of { namespace, name, qualifiedName, description }, sorted by name. |
catalog.tools.describe | namespace, name | { namespace, name, qualifiedName, description, inputSchema, outputSchema } |
catalog.toolkits.list | limit?, offset? | Page of { name, description, toolCount }, sorted by name. |
catalog.toolkits.get | name | { name, description, content }. content is text: the toolkit's instructions, then each member tool's qualified name, invocation, description and input and output schemas. |
- Search uses the gateway's tool search ranking (BM25, or remote embeddings when configured) over every permitted tool, whatever its disclosure mode.
limitdefaults tosearch.max_allowable_results(5 unless configured) and is clamped to it. - Pages have the shape
{ items, total, limit, offset, hasMore, nextOffset }.limitdefaults to 20 and is capped at 100.nextOffsetis present only whenhasMoreis true. - Toolkit visibility. A toolkit is listed and returned only when every tool in it exists and the caller may use all of them.
console
console.log, console.info, console.warn, console.error and console.debug each append one line to the result's logs. Arguments are joined with spaces, and anything other than a string is JSON-encoded. Each call counts toward max_host_calls. At most 128 lines are kept, and a line longer than 4096 bytes is truncated.
Call envelopes
Tool and catalog calls resolve to one of two shapes and never throw for a failed call:
{ "ok": true, "data": { "id": "c-1042" }, "meta": { "omitted": 0 } }
{ "ok": false, "error": { "code": "access_denied", "message": "Access to the requested tool was denied", "retryable": false } }
For tool calls, data is the tool's structured result when it has one: a REST tool with an output_schema, or an upstream MCP result with structuredContent. Otherwise data is the result's text content as a string (blocks joined by newlines), or null. meta.omitted counts non-text content blocks, such as images, that the envelope dropped. Catalog envelopes carry data only.
Tool call error codes
code | Cause |
|---|---|
tool_not_found | The name is not a valid qualified name, or no such tool is registered. |
access_denied | The tool's rbac denies the caller. |
invalid_input | The arguments do not match the tool's input schema, or the call is malformed. |
upstream_error, upstream_timeout, response_too_large, overflow, invalid_output | The backend failed. The gateway's tool error is passed through, with its retryable flag and HTTP status. upstream_error also covers an unreachable upstream MCP server. |
result_too_large | This call's serialized result is larger than max_output_bytes. |
Catalog error codes
code | retryable | Cause |
|---|---|---|
invalid_input | false | A missing or invalid parameter, such as an empty query or limit: 0. |
not_found | false | The tool or toolkit does not exist, or the caller may not see it. |
catalog_unavailable | true | The registry could not be brought up to date, for example because a dynamic server's refresh failed. |
The execute result
A run always returns a normal tools/call result whose structuredContent looks like this:
{
"status": "completed",
"result": { "ticket": "T-8821" },
"error": null,
"diagnostics": [],
"logs": ["recent tickets: 3"],
"toolCalls": [
{ "tool": "crm.find_customer", "ok": true, "durationMs": 38 },
{ "tool": "support.recent_tickets", "ok": true, "durationMs": 52 },
{ "tool": "billing.current_plan", "ok": true, "durationMs": 47 },
{ "tool": "support.create_ticket", "ok": true, "durationMs": 61 }
]
}| Field | Description |
|---|---|
status | completed or error. On error the result also sets isError: true. |
result | The script's return value, or null on error. |
error | The diagnostic on error, otherwise null. diagnostics holds the same object in a list. |
logs | Lines from console, kept on error too. |
toolCalls | Every tool call in submission order, with ok and durationMs. Catalog and log calls are not listed. |
If the whole object is larger than max_output_bytes, the gateway keeps result and drops entries from the end of toolCalls, then from logs, until it fits. The result's _meta then carries "arion/codemodeTruncation": { "logsOmitted": N, "toolCallsOmitted": N }. If result alone does not fit, the run reports a ResourceLimitExceeded error instead.
Diagnostics
kind | When |
|---|---|
ParseError | The script has a syntax error or unsupported TypeScript. Carries line and column in the script as sent. |
ExecutionFailure | An exception or rejection escaped the script. The message includes the stack. |
TimeoutExceeded | execution_timeout, or the call's timeout_ms, elapsed. |
ResourceLimitExceeded | The compute budget, host-call budget, result size or script size was exceeded. The message names which one. |
InternalError | Something failed inside the gateway. It is reported generically, so no host details leak. |
Errors outside the result
These come back as JSON-RPC errors, not as an execute result:
| Code | Message | Cause |
|---|---|---|
-32600 | Too many Code Mode executions are active | The listener is running max_concurrent_executions scripts. Retry later. |
-32603 | Json schema validation error: … | The execute arguments are invalid. |
-32002 | Tool '<name>' not found | The tool called was not execute. |
Limits
| Limit | Default | Set by | When exceeded |
|---|---|---|---|
| Wall-clock time per run | 30 s | code_mode.execution_timeout; lowered per call by timeout_ms | TimeoutExceeded |
| Compute time per run | 5 s | Fixed | ResourceLimitExceeded. Time spent waiting on tool and catalog calls does not count. |
| Sandbox memory | 64 MiB | code_mode.memory_limit_bytes, at most runtime.codemode.max_memory_bytes | Allocation fails inside the script. |
| Host calls per run (tools, catalog, logs) | 100 | code_mode.max_host_calls | The call throws, and the run ends with ResourceLimitExceeded even if the script catches it. |
| Runs at once per listener | 32 | code_mode.max_concurrent_executions, at most runtime.codemode.pool_capacity | JSON-RPC error -32600; runs are not queued. |
| Calls in flight within a run | 8 | code_mode.max_concurrent_tool_calls | Extra calls wait their turn. |
| Result size | 1 MiB | code_mode.max_output_bytes, at least 1024 | A single call: result_too_large. The final result: truncation, then ResourceLimitExceeded. |
| Script size | 256 KiB | Fixed | ResourceLimitExceeded, before any sandbox starts. |
| Log lines | 128 lines of 4096 bytes | Fixed | Extra lines are dropped and long lines truncated. |
| Sandboxes per process | 128 | runtime.codemode.pool_capacity or ARION_CODEMODE_POOL_CAPACITY | A listener that needs more fails to load. |
| Memory ceiling per sandbox | 64 MiB | runtime.codemode.max_memory_bytes or ARION_CODEMODE_MAX_MEMORY_BYTES | A listener that asks for more fails to load. |
Numeric code_mode fields left at 0 take their defaults.
The sandbox
- Engine. Scripts run in a WebAssembly component on Wasmtime: QuickJS compiled for WASI and initialized ahead of time with Wizer, so each run starts from a snapshot rather than booting an interpreter.
- Pool. Instances come from a pooling allocator sized by
runtime.codemode.pool_capacity. The thread that enforces the compute budget runs only while a script is executing, so an idle gateway does no background work. - Isolation. The sandbox has no preopened directories, no environment variables and no sockets. Its only link to the host is an interface with three calls:
request(a log line),submit(start a tool or catalog call) andnext-completion(wait for one to finish). Every tool call lands on the same registry checks as a normaltools/call. - Custom engine.
code_mode.engine_artifact_pathreplaces the bundled component with your own build. When the listener loads, Arion checks the component's imports against the allowed set and rejects anything else.