Skip to content

07 — MCP Architecture

This is the Model Context Protocol implementation chapter: the server classes, the tools they expose, the permission machinery that gates every call, and the session lifecycle that wraps each turn.

§04 — Core Concepts introduced MCP as the contract between a host that runs LLMs and a server that exposes tools, and noted that The Aetherium Engine hosts its MCP servers in-process — the wire protocol collapses into method calls on classes registered in the DI container, but the schema discipline is unchanged. §06 — Key Services showed the McpToolRouter sitting in the middle of that flow. This section is the full surface: what every tool is, how a tool earns its permission tier, and how a tool call lives and dies.

It is organized as a capability map, then a catalog grouped by server class, then the permission machinery at mechanism depth, then the session lifecycle. The catalog is the single most verifiable artifact in this doc set — the live ground truth is one HTTP call away. Hit GET /api/tools/mcp-schemas on a running instance and diff it against the tables below; the endpoint returns the same catalog the router assembles at startup, filtered per agent.

The capability map

The Engine ships 44 tools across 12 MCP server classes. The router sits above all of them as the single dispatch and permission point; the classes group naturally into five capability domains.

flowchart TB
    Router[McpToolRouter<br/>schema discovery · dispatch · gates]:::router
    Router --> Exec
    Router --> Know
    Router --> Coord
    Router --> QM
    Router --> Dev

    subgraph Exec[Execution]
        WT[WorkspaceTools · 6]:::cls
        ST[ShellTools · 2]:::cls
    end
    subgraph Know[Knowledge]
        KT[KnowledgeTools · 2]:::cls
        RT[ResearchTools · 6]:::cls
    end
    subgraph Coord[Coordination]
        PT[PlanningTools · 4]:::cls
        CT[CommunicationTools · 2]:::cls
        OT[OrchestrationTools · 3]:::cls
        TT[TeamMessagingTools · 1]:::cls
    end
    subgraph QM[Quality &amp; Media]
        QT[QualityTools · 8]:::cls
        CU[ComfyUITools · 4]:::cls
        AU[AudioTools · 2]:::cls
    end
    subgraph Dev[Devices]
        DT[DeviceTools · 4]:::cls
    end

    classDef router fill:#B87333,stroke:#2D2D2D,stroke-width:2px,color:#FFFFFF
    classDef cls fill:#8FA68A,stroke:#2D2D2D,stroke-width:1.5px,color:#1A1A1A

Execution owns the workspace: reading and writing files, running shell commands inside the project container. Knowledge owns retrieval: the ChromaDB knowledge base and outbound web research. Coordination owns the multi-agent machinery: planning, user notifications, delegation, and peer messaging. Quality & Media owns validation, image and video generation, and voice-over. Devices acts on hardware: the Steam Frame headset. Every class carries the [McpServerToolType] attribute and is registered with a .WithTools<T>() call in Program.cs — that registration list is the authoritative roster. Each MCP server class is a C# class, not a separate running process; "MCP server class" throughout this section means the class, hosted in-process by the Engine.

The tool catalog

Each subsection below is one MCP server class: its role, its tools with one-line purposes and permission tiers, and anything notable. Full parameter signatures are deferred to the Technical Reference doc set; for the canonical live schema, use the endpoint named above.

Execution

WorkspaceTools

File operations on the project workspace, routed through the workspace executor into the container.

Tool Purpose Tier
read_file Read and return a file's contents Standard
write_file Create or overwrite a file Standard
edit_file Replace a search string with new text in place Standard
list_files List the workspace as a directory tree Standard
revert_file Undo this session's edits to a file Standard
search_files Find files by name pattern and lines by content, read-only, output capped Standard

edit_file is search-and-replace first: exact match, then a whitespace-tolerant match when that lands in exactly one place. It also creates a missing file when search is empty, and takes an optional start_line/end_line range to replace those lines outright or to confine the search; a range from line 1 to the end with an empty search is the whole-file rewrite, with an end_line past the end clamped to the last line and no shrink guard in the way. A miss reports the nearest line so the agent can correct the search text without a re-read, and names the whole-file form when the search was most of the file; a multi-match replace whose insertions would outweigh the text is refused rather than repeated. search_files is the read-only locate step — file names by glob, lines by text or regex — meant to replace run_shell with ls, find or grep. revert_file restores the pre-edit snapshot the Engine holds for the duration of a session — the same in-session snapshot mechanism described in §06. All six are Standard; file I/O stays inside the workspace, so it carries no network or escalation risk.

ShellTools

Runs commands and reads logs inside the project's Docker container.

Tool Purpose Tier
run_shell Execute a shell command in the workspace container Elevated
read_logs Read Docker, systemd, or file logs Elevated

Commands run through the container executor, not the host shell, so run_shell is sandboxed to the project's own filesystem and process space. It is Elevated and escalates to a human-approval gate when the command matches a destructive pattern (rm -rf, chmod, chown, sudo, docker rm/stop/restart, dotnet publish, edits to appsettings, and similar). Every invocation is recorded to a command-log table.

Knowledge

KnowledgeTools

Reads and writes the ChromaDB knowledge base described in §06.

Tool Purpose Tier
search_knowledge Semantic search over the agent's knowledge scope: its team's packs, domains and project Standard
write_knowledge Save a document into the project knowledge base Standard

search_knowledge searches the scope the Engine resolved for the calling node at session start, and discards hits under ChromaDb:ScoreFloor (§09 — Configuration). write_knowledge returns a structured JSON receipt so a worker can confirm the save succeeded and stop retrying, and it persists the file to the workspace even when ChromaDB indexing fails — a deliberate resilience choice, since a transient index error should not cost a worker its output.

ResearchTools

Outbound web and HTTP, SearXNG-backed with a DuckDuckGo fallback.

Tool Purpose Tier
web_search Search the web via SearXNG Elevated
web_fetch Fetch a page and strip it to readable text Elevated
http_request Issue an arbitrary HTTP request Elevated
http_health_check Ping an endpoint; return status and latency Elevated
download_pdf Download a PDF and extract its text Restricted
capture_reference Screenshot a page and return its computed design facts Elevated

web_fetch strips scripts, styles, and markup and truncates very long pages, so the model receives readable text rather than raw HTML; http_request covers the standard verbs (GET, POST, PUT, DELETE, PATCH, HEAD). Every tool here reaches the network, so all are at least Elevated and pass the egress/DLP gate (below). download_pdf is the Engine's single Restricted tool — it always requires human approval with no supervisor fallback. capture_reference is the odd one out: it drives headless Chrome rather than an HTTP client, and what it returns is the page's computed design facts — palette by share of painted area, type scale, measure, rhythm, breakpoints — alongside screenshots saved into the workspace, so a design decision can be argued from evidence instead of recollection. The SearXNG backend is covered in §13 — Integrations.

Coordination

PlanningTools

The supervisor's execution plan — see Plans in §04.

Tool Purpose Tier
create_plan Create a step list for the task Standard
read_plan Read the active plan (next step, or all) Standard
update_plan Mark a step's status and record its result Standard
todo An agent's own scratch list for the run, separate from the team plan Standard

Plans persist through the planning service, so read_plan can return either the next actionable step or the whole list, and update_plan records each step's result as the supervisor works through the plan in order. todo is per agent and per run: write replaces the list, check marks an item done, read shows it. It never touches the team plan.

CommunicationTools

User-facing messages surfaced through the real-time layer.

Tool Purpose Tier
notify_user Send a notification about progress or an event Standard
report_progress Report task progress with a percent-complete Standard

Both publish through the real-time layer (§06) — they are how an agent's status reaches the Studio UI mid-run rather than only when the turn ends.

OrchestrationTools

Multi-agent coordination.

Tool Purpose Tier
delegate Hand a task to a named worker agent Standard
reply Send the final response to the user Standard
spawn_subagents Spawn parallel research subagents Elevated

spawn_subagents is Elevated because it multiplies LLM load — it is the engine behind the dynamic-parallel escape hatch from §06.

TeamMessagingTools

Peer-to-peer messaging between agents on a team.

Tool Purpose Tier
send_message_to Send a direct message to a teammate's mailbox Standard

Delivery is asynchronous — a message lands in the target agent's mailbox and is read on that agent's next turn, which lets peers coordinate directly instead of routing everything through the supervisor.

Quality & Media

QualityTools

Code analysis and validation.

Tool Purpose Tier
analyze_code Static analysis of a file or path Standard
code_review Structured review of a path Standard
run_tests Run the project's test suite Standard
validate_web_app Check a running web app against a set of checks Standard
capture_api_schema Capture an API's schema from its endpoints Standard
record_style Write the style record, generating its token tables from a stylesheet Standard
check_finishing Check the deterministic marks of a finished project Standard
play_test Open a web page in a browser, click each control once, report what its script throws Standard

analyze_code auto-selects a linter by file extension — dotnet format for C#, ESLint for JavaScript and TypeScript, pylint or flake8 for Python — and reads the file itself, so an agent should not call read_file first. code_review is the LLM-backed one: it hands the file to a model through the scheduler and returns a structured, severity-tagged review rather than linter output. run_tests detects the framework (npm/jest, pytest, or dotnet test) and parses the pass/fail counts out of the run. validate_web_app and capture_api_schema reach a running application — they are how a QA agent confirms that a worker's web app actually serves and that its API matches the agreed shape, rather than only reading the source. The last two are about a project being finished rather than correct: check_finishing returns a PASS/FAIL row per deterministic mark (a favicon every page links, no leftover scaffold markers, a README whose screenshots exist, a version matching the changelog head, an empty state on every list screen), reading only files that ship; record_style takes the approved direction and the rules a later run must not break as prose, then generates the token tables itself from the stylesheet, so the record cannot document a palette the code does not contain. play_test opens a page the way a visitor does, clicks each visible control once and reports every error its script throws; a QA role that holds it and states a verdict on a page it has not played, as that page stands now, is sent back.

ComfyUITools

Image generation via ComfyUI — see §13 — Integrations.

Tool Purpose Tier
comfyui_generate Queue a ComfyUI workflow Standard
comfyui_status Poll a generation's status Standard
finish_image Queue the final 2K upscale of Qwen-Image pictures, after the run Standard
comfyui_video Render a 1-5 s Wan 2.2 clip from a description or a workspace image and save it as an MP4 (server group video) Standard

comfyui_generate queues a workflow on the ComfyUI backend over its REST/WebSocket API; by default it waits, saves each finished image into the run's workspace (save_dir, default images/, under filename when one is given) and returns the /workspace/…png paths, or with wait=false returns the prompt ID for comfyui_status to poll. A team whose config sets image_model: "qwen" renders on Qwen-Image 2.1 through its own leased service, comfyui-qwen, and gains ratio, transparent and reference_images; finish_image queues a slow upscale of that team's pictures for after the run (quality and media tools). comfyui_video is in its own server group, video (the video tool profile), and renders on the comfyui-video GPU service, which the run's lease starts (quality and media tools).

AudioTools

Voice-over through the Engine's voice service (Qwen3-TTS under vllm-omni), started by the run's lease when the template declares gpu_services: ["qwen-tts"].

Tool Purpose Tier
tts_render Speak a workspace script file into one MP3 Standard
tts_status Whether the voice service is up, and the reference voices Standard

tts_render reads the script (speaker lines in any of the shapes agents write: [HOST] then PIXEL: words, NAME: words, **NAME:** words, **Host:** then the words below it), speaks each line in the reference voice mapped to its speaker, joins the lines with short gaps and writes the MP3 into the workspace — the audio becomes a node's own deliverable instead of a step run after the build. The same parser decides the spoken-word count a required_output can demand (see §08). A voice in the cast is either a reference clip or a written description (design: …), which the voice design service turns into a clip once (tools reference).

Devices

DeviceTools

The Steam Frame headset (server group device), the Engine's first tools that act on hardware. Only the roles of the steam-frame-vr template are given the group, so no other team's tool list changes. The work is done by the scripts in device/steam-frame/ beside the Engine; an agent never gets a raw adb or ssh, and every value it supplies is checked before it reaches a command on the headset.

Tool Purpose Tier
frame_status Read the headset's state without changing anything: reachable and paired, its Android container up, adb connected, which build is installed, how many saves it has Standard
frame_install Put an APK on the headset (arm64-v8a, Android API 30, data inside the APK), starting the container and reconnecting adb when needed, and put the app's saves back after a container restart Restricted
frame_run Run the app with someone wearing the headset: capture the headset view every few seconds, wait for them to exit, then bring back the log, the performance recording, the saves and a contact sheet, with how the run ended and the frame rate against the 72 fps Verified bar Restricted
frame_grab One picture of what the headset is showing right now Restricted

One call at a time holds the headset; a second is refused while the first runs. Everything a run captures lands under .frame/ in the workspace. The frames show the wearer's room through passthrough, so that dot directory is skipped by every knowledge-base path and nothing captured is offered to or indexed in a knowledge base. Connection settings are under Aetherium:Devices:SteamFrame (§09 — Configuration).

That is 44 tools: 32 Standard, 8 Elevated, 4 Restricted.

The three-layer permission model

§04 named the three layers and §06 drew the dispatch decision tree; this section is the mechanism behind them, plus the four security services that enforce it. The layers, recalled briefly: connection filters which server classes an agent can reach, capability filters which tools within those classes, and runtime gates the call by security tier.

Connection and capability layers

The connection layer is the agent's mcp_servers allowlist. Each agent declares the server-group keys it is allowed to see:

"mcp_servers": ["workspace", "shell", "research", "team", "orchestration"]

The thirteen group keys are workspace, shell, research, knowledge, quality, planning, comms, orchestration, team, comfyui, video, audio, and device. The router's ToolToServerGroup map decides which tools belong to which group; an agent that omits shell never sees run_shell in its catalog — unless its own tools list names run_shell: since 2026-10-13 a node is served from its mcp_servers plus the group of every tool its tools list names, because 35 of the shipped roles listed a tool (mostly todo or reply) whose group their mcp_servers left out, and the connection layer hid a tool the runtime allowlist accepts. A node with no mcp_servers gets them derived from its tools. Like the tier map, this group map is static code, so a tool's server group and its security tier are both fixed at compile time rather than configured per deployment. These two maps are part of why §12 — Extending the Engine treats registering a new tool as a multi-step code change — four edits in all, plus an optional fifth for the tier. Authoring these allowlists belongs to §08 — Templates.

The capability layer narrows further. Within an allowed group, only the specific tools the agent declares are kept, and the router strips the rest from the tool schema before the LLM sees it — the model cannot call a tool it was never shown. This matters more than a runtime block would: a tool absent from the catalog cannot be hallucinated into existence, so the capability layer prevents whole classes of misbehavior rather than catching them after the fact. (Supervisors are the one exception: the router always injects delegate and reply so a supervisor can coordinate even with a minimal allowlist.)

Runtime layer — security tiers

Every tool carries one of three tiers, assigned in a single static map (ToolSecurityLevels) inside the router. This is the authoritative source — a tool's tier is code, not configuration. Only the non-Standard tools are listed in the map; the lookup defaults anything absent to Standard, which is why adding a new tool without touching the map silently makes it Standard:

  • Standard — executes immediately with a basic audit record. 32 of the 44 tools.
  • Elevated — executes with enhanced audit (full arguments and result captured). The eight network-and-load tools: web_search, web_fetch, http_request, http_health_check, capture_reference, run_shell, read_logs, spawn_subagents.
  • Restricted — blocks for human approval before executing. Four tools: download_pdf, and the three device tools that act on the headset or capture what its wearer sees: frame_install, frame_run, frame_grab.

The tiers track blast radius. Standard tools are reversible and local — file edits inside the workspace, plan updates, knowledge writes — so they run with only a basic record. Elevated tools either reach the network (web_search, web_fetch, http_request, http_health_check, capture_reference) or amplify load and command the host environment (run_shell, read_logs, spawn_subagents), so they run with the full arguments and result captured for replay. Restricted is reserved for an action with an irreversible external side-effect — pulling a remote file to disk, or acting on a headset someone is wearing — which is why it stops for a human.

The Restricted approval flow is handled by ToolApprovalService (§06). A request is emitted, a human has a default five-minute window to approve, and the call auto-denies on timeout — supervisor fallback is off by default, so a Restricted tool cannot be waved through by another agent.

YAML permission rules

PermissionRulesService layers declarative rules on top of the tiers, loaded from config/permissions.yaml and hot-reloaded through a file watcher (a debounced reload, so saving the file mid-run takes effect without a restart). A rule matches on any combination of tool name, team, agent, and argument/path/URL pattern, and names an action:

rules:
  - match:
      tool: run_shell
      arg_pattern: "rm -rf|dd if=|mkfs"
    action: always_deny
    reason: "Destructive shell pattern blocked by policy"

The enforced action today is always_deny — a matching rule blocks the call before it reaches the tier gate, via the same pre-execution hook the dispatch tree showed in §06. The default ruleset ships empty; the file is the seam for site-specific policy. (always_allow and ask_user actions parse but are advisory in the current build — treat always_deny as the load-bearing capability.)

Egress and DLP

EgressPolicyService is the pre-execution hook on the eight outbound tools — web_search, web_fetch, http_request, download_pdf, capture_reference, http_health_check, validate_web_app, and capture_api_schema. Membership of that set is the whole gate: a URL tool missing from it is not checked at all, so the set is derived from the tool schema by a test rather than maintained by hand. It does two things. First, it checks the destination against the team's domain allowlist (carried on the session context), always permitting loopback and private-range addresses and matching configured domains exactly or by wildcard. Second, it scans the outgoing arguments — body, headers, URL, query — for credential shapes (provider API keys, cloud access keys, version-control tokens, SSH private keys, JWTs) and blocks the call if it finds one, so a leaking secret never leaves the box. The allowlist is configured per team; §09 — Configuration covers the keys.

Audit trail

ToolSecurityAuditService writes every gated call to a PostgreSQL audit table. Each entry carries the timestamp, project, team, agent, tool name, security level, whether it was allowed, who approved it, and the execution time; Elevated and Restricted calls additionally capture the full arguments and result, while Standard calls keep a truncated summary. Writes happen post-execution and on any block or denial, so the table is a complete record of what every agent tried and what the machinery let through. Retention is configured in §09 — Configuration.

There is no per-tool call ceiling (how many times one tool may run in a session): the circuit breaker's per_tool_limits map was never counted and was removed on 2026-10-08.

Schema discovery and the session lifecycle

The tool catalog is assembled once, lazily, on first access. The router reflects over every [McpServerTool] method registered in the DI container, reads each tool's name, description, and input schema, and converts them into the function-calling format the LLM expects. The result is cached — tool schemas do not change at runtime, so the reflection cost is paid exactly once per process. GetToolDefinitions then produces the per-agent view by applying the connection and capability filters, and — when the node declares an explicit tools allowlist — additionally intersecting the schemas with it, so the model is never shown a tool the runtime would reject (delegate and reply stay visible to supervisors, as control flow rather than work tools). The catalog the model receives is already scoped to what that agent may do; the same filtered shape is what GET /api/tools/mcp-schemas returns when you ask it for a given server set.

A boot-time guard keeps the catalog honest. ToolCatalogValidator runs as a hosted service at startup and asserts that the router's registered tool set matches the set the prompt-enhancer documents — if a tool is advertised in a system prompt but not routable, or vice versa, the Engine fails fast rather than shipping a catalog that lies to the model.

At runtime, each tool call rides a session context. McpSessionContext (defined in the shared library) carries the project ID, agent identity, container ID and workspace path, the authorized server groups, the allowed domains, any per-message argument defaults, and the agent's knowledge scope and team image model (so a tool never has to look the caller's team up). The context is made ambient through an AsyncLocal provider so tool code can read it without it being threaded through every signature.

flowchart TB
    Begin([Agent turn begins]):::ext --> Build[Assemble McpSessionContext<br/>project · agent · workspace ·<br/>authorized servers · allowed domains]
    Build --> Filter[Filter tool catalog<br/>connection + capability layers]
    Filter --> Q{Agent emits<br/>a tool call?}
    Q -- no --> Done([Turn ends]):::ext
    Q -- yes --> Set[Set ambient context<br/>via McpSessionProvider]
    Set --> Gate[Tier gate + egress/DLP + approval]
    Gate --> Dispatch[Dispatch to MCP server class method]
    Dispatch --> Clear[Clear context · write audit entry]
    Clear --> Q

    classDef ext fill:#D5C3B0,stroke:#2D2D2D,stroke-width:1.5px,color:#1A1A1A
    classDef gate fill:#B87333,stroke:#2D2D2D,stroke-width:2px,color:#FFFFFF

Reading it start to finish: the turn begins, the Engine assembles the McpSessionContext for this agent, and filters the catalog down to what the agent is allowed to see. For each tool call the agent emits, the context is set as ambient state, the call passes the tier, egress/DLP, and approval gates, and — if it survives — dispatches to the owning MCP server class method. The context is cleared and an audit entry written in a finally block, so cleanup and the audit record happen whether the call succeeded or threw. The loop repeats until the agent emits a turn with no tool calls, and the turn ends.

Where to go next