# Tool Gateway

The Tool Gateway is the MCP hub, `speedwave_<project>_mcp_hub`, the only MCP server Claude sees. Whatever integrations you enable, Claude gets exactly two tools: `search_tools` and `execute_code`.

## The two tools

`search_tools` discovers tools across all enabled integrations, including OS tools. `execute_code` routes execution to the right [worker](/docs/under-the-hood/workers/), with each integration exposed as a named object like `redmine.listIssueIds()`.

```js title="Conceptual execute_code snippet (shape, not a fixed API)"
// Claude has already used search_tools to locate these calls.
const ids = await redmine.listIssueIds({ project: "speedwave" });
const issues = await batch(ids.map((id) => redmine.getIssue({ id })));
return issues.results;
```

Treat the named objects here as illustrative, not a stable public contract.

1. Claude calls `search_tools` with a keyword, and the hub returns matching tool names and descriptions.

2. Claude writes a short script that calls the tools it found.

3. Claude calls `execute_code` with that script, and the hub runs it in a sandbox, routes each call to the right worker, and returns the result.

A tool stays hidden until `search_tools` surfaces it, keeping token usage low on large projects. A worker can opt into upfront visibility with `_meta: { deferLoading: false }`.

## Zero tokens at the hub

Each worker mounts only its own service credentials, read-only; the hub itself holds none. See [Isolation model](/docs/security/isolation/) for why that holds. OS tools differ: the hub reaches them on the host through an HTTP bridge that resolves only known service names.

```mermaid
flowchart LR
  Claude["Claude"] -->|search_tools| Hub
  Claude -->|execute_code| Hub["MCP hub (port 4000, zero tokens)"]
  Hub -->|"gitlab.getMrFull()"| GL["gitlab worker (/tokens:ro)"]
  Hub -->|"redmine.listIssueIds()"| RM["redmine worker (/tokens:ro)"]
  Hub -->|"os.createEvent() via HTTP bridge"| OS["OS tools on host"]
```

## A sandbox, not a general code runner

`execute_code` blocks dangerous APIs, `eval`, `require`, `process`, filesystem and network access, before the script runs, and exposes only a small whitelist of globals such as `JSON` and `Array`.

Heavier sandboxes were tried and rejected: `isolated-vm` was in maintenance mode with binaries mismatched to Node 24's ABI, `quickjs` broke `batch()` and `Promise.allSettled` across async suspension, and Node's `vm` module was never a real security boundary. That history is why `execute_code` runs on an AsyncFunction with a denylist, backed by the hardened container as the real boundary.
**Caution:** Treat these limits as defense in depth, not a substitute for reviewing what an integration can reach.

Escaping the sandbox still lands in a hardened container with nothing to steal, and any calls made afterward show up in the audit log.

## Errors that help the model fix itself

Before an error reaches Claude, the hub sanitizes it: file paths become `[file]`, line numbers are stripped, and the message is capped at 500 characters. An unknown method lists every real method on that object, and `service_method` gets a camelCase fix suggested. Each error also carries a retryable flag, true for timeouts. This is in-session only; the hub keeps no memory between sessions.

## Tokenization and auditing

The hub also masks results before Claude sees them. See [Tokenization](/docs/security/tokenization/) for how that works. It logs each `execute_code` call: its kind, the integration reached, and its parameters.

## One discovery path for every worker

Built-in workers and plugins share the same discovery path and `_meta` policy; there is no hardcoded list of built-in tools. A worker down at hub startup leaves an empty entry until a background refresh finds it. See [Workers](/docs/under-the-hood/workers/) for what a worker container is.