Skip to content

MCP Server

santisoutoo edited this page Jul 24, 2026 · 1 revision

MCP Server

a320-mcp is the LLM's window onto the same core the CLI-Reference drives — two frontends, one API. The agent flies the closed loop: observe the ECAM, reason over the procedure, act on the switches, advance time, observe again. Every tool is a 1:1 mapping onto a320_sim.Sim; there is no simulation logic in this package.

Run it

a320-mcp                            # cold & dark (default)
a320-mcp --start apu-running        # APU started and feeding the AC network
a320-mcp --start engines-running    # both engines at idle powering everything

The server speaks stdio, so you don't normally run it by hand — a client launches it and talks over the pipe. The scenario (the start state) is the harness's job, not the agent's:

  • --start apu-running takes ~60 s of simulated time at boot (a few seconds of wall clock) to spin the APU up before serving.
  • --start engines-running runs the full cold & dark → engines running sequence (~6 min simulated: APU, APU bleed, both engine starts, APU shutdown) and hands over a healthy aircraft on its engine generators with a clean ECAM.

Point a client at it

The repo ships a .mcp.json that registers the server:

{
  "mcpServers": {
    "a320": {
      "command": "a320-mcp",
      "args": ["--start", "apu-running"]
    }
  }
}

The client spawns a320-mcp, so it must be on the PATH of the process that launches the client — activate the virtualenv you installed it into, or point command at the executable directly (.venv/Scripts/a320-mcp.exe on Windows, .venv/bin/a320-mcp elsewhere). MCP servers are read at client startup, so restart an already-running client to pick it up.

Then hand it the scenario in plain language:

The APU generator just failed. Deal with it.

The agent has the ECAM and the switches — and nothing that tells it what broke.

Tools

Tool What it does
read_ecam Active warnings and cautions, worst first — the agent's primary observation
read_state(variables) Read named state variables (takes a list)
snapshot(contains) Discover state variables by name filter (rejects a filter matching > 60 vars)
list_controls The curated cockpit controls you can actuate
list_failures The catalog of what can break
set_control(control, value) Flip a switch or pushbutton
advance(seconds, rate=5.0) Advance simulated time — nothing takes effect until you do (capped at 600 s/call)
inject_failure(failure_id) Break something (reversibly) — interactive profile only
clear_failure(failure_id) Repair it — interactive profile only

Valid control names and failure ids are baked into the tool schemas as enums generated from the catalogs, so the model cannot name something that isn't there.

What deliberately does not exist

The core can report which failures are active (active_failures()) and can list every variable (list_variables()); neither is exposed as a tool, and not by omission:

  • active_failures would hand the agent the answer it is supposed to diagnose from the ECAM — the benchmark would measure reading, not diagnosis.
  • list_variables is hundreds of names and would bury the context window.

So snapshot(contains=…) is the output-discovery mechanism. See Design-Decisions (D-016).

Tool profiles

The table above is the interactive profile — the full 9-tool surface the stdio entry point serves. The server is a factory, create_server(sim, profile=…):

Profile Tools
interactive (default) the 9 tools above, including inject_failure / clear_failure
benchmark withholds inject_failure / clear_failure, adds report_done(diagnosis, actions_summary)

The Phase-5 runner builds a benchmark-profile server: the injected failure is the exam, so an agent that can repair the fault (or break something else) is not flying the procedure, and report_done is the explicit end-of-episode channel. Profiles change which tools exist, never what a tool does. See Running-the-Benchmark and Design-Decisions (D-023).

Implementation note. Tools are plain synchronous functions, and that is load-bearing: the Sim is unsendable (the aircraft uses Rc/RefCell internally), so it must be touched only from the event-loop thread. advance blocks on purpose — with stdio there is a single client and nothing else to serve. See D-010 / D-015.

The agent loop

> read_ecam
  []                                      # healthy: the APU feeds the network

  (a failure is injected by the harness)

> read_ecam
  [{"message": "AC ESS BUS FAULT", "severity": "caution", "system": "ELEC", "source": "vendor_flag"},
   {"message": "APU GEN FAULT",    "severity": "caution", "system": "ELEC", "source": "vendor_flag"}]

> snapshot "_BUS_IS_POWERED"
  {"ELEC_AC_1_BUS_IS_POWERED": 0.0,       # the whole AC network is gone...
   "ELEC_DC_1_BUS_IS_POWERED": 0.0,       # ...and DC 1/2 with it (they feed via the TRs)
   "ELEC_DC_BAT_BUS_IS_POWERED": 1.0,     # batteries hold the essential DC —
   "ELEC_DC_ESS_BUS_IS_POWERED": 1.0}     # which is why the ECAM is still readable

> set_control apu_gen 0                    # the faulty source out of the loop
> set_control ext_pwr_avail 1              # ask for a GPU
> set_control ext_pwr 1                    # put it on the network
> advance 3
> read_ecam
  []                                       # clear: AC 1/2/ESS and DC 1/2 all back

Losing the APU generator raises two cautions — the source fault and the downstream AC ESS bus — because it was the only AC source. That cascade is the scenario, and dealing with it is the task.

Tests

The server tests spawn it as a subprocess and drive it over the real stdio protocol, so what is checked is what an agent would see:

python mcp/tests/test_server.py     # or: pytest mcp/tests/

Clone this wiki locally