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
Section titled “The two tools”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().
// 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.
-
Claude calls
search_toolswith a keyword, and the hub returns matching tool names and descriptions. -
Claude writes a short script that calls the tools it found.
-
Claude calls
execute_codewith 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
Section titled “Zero tokens at the hub”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"]
A sandbox, not a general code runner
Section titled “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.
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
Section titled “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
Section titled “Tokenization and auditing”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.
One discovery path for every worker
Section titled “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 for what a worker container is.