Skip to content

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.

search_tools discovers tools across all enabled integrations, including OS tools. execute_code routes execution to the right worker, with each integration exposed as a named object like redmine.listIssueIds().

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 }.

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

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"]

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.

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

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.

The hub also masks results before Claude sees them. See Tokenization for how that works. It logs each execute_code call: its kind, the integration reached, and its parameters.

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 for what a worker container is.