---
title: "Code mode SDK and limits"
url: "/reference/sdk/code-mode-sdk-and-limits/"
kind: "reference"
summary: "What a code mode script can call, the catalog operations and their results, call envelopes and error codes, the execute result, and every limit."
---

> Arion site index: [/llms.txt](/llms.txt)

# Code mode SDK and limits

What a code mode script can call, the catalog operations and their results, call envelopes and error codes, the execute result, and every limit.

{{ <badge label="requires: codemode" kind="requires" /> }}

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](@/docs/mcp-gateway/code-mode.md) 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 `async` function, so `await` works at the top level and `return` ends the run. The returned value is serialized as JSON, and `undefined` becomes `null`.
- **Strip-only TypeScript.** Type annotations, interfaces and type aliases are removed without moving any line or column. `enum`, `namespace` declarations and decorators are rejected with a `ParseError`.
- **Engine.** The engine is QuickJS. Standard ECMAScript built-ins such as `JSON`, `Promise`, `Map` and `Math` are available. There is no `fetch`, no `setTimeout`, 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](#call-envelopes). |
| `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](@/docs/mcp-gateway/tool-search.md) ranking (BM25, or remote embeddings when configured) over every permitted tool, whatever its disclosure mode. `limit` defaults to `search.max_allowable_results` (5 unless configured) and is clamped to it.
- **Pages** have the shape `{ items, total, limit, offset, hasMore, nextOffset }`. `limit` defaults to 20 and is capped at 100. `nextOffset` is present only when `hasMore` is 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:

```json
{ "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](@/docs/mcp-gateway/tools-and-rest-backends.md#validate-the-response) 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:

```json
{
  "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`](@/reference/configuration/file-format.md#runtime) 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) and `next-completion` (wait for one to finish). Every tool call lands on the same registry checks as a normal `tools/call`.
- **Custom engine.** `code_mode.engine_artifact_path` replaces 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.
