-
Notifications
You must be signed in to change notification settings - Fork 0
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.
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 everythingThe 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-runningtakes ~60 s of simulated time at boot (a few seconds of wall clock) to spin the APU up before serving. -
--start engines-runningruns 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.
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.
| 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.
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_failureswould hand the agent the answer it is supposed to diagnose from the ECAM — the benchmark would measure reading, not diagnosis. -
list_variablesis hundreds of names and would bury the context window.
So snapshot(contains=…) is the output-discovery mechanism. See Design-Decisions (D-016).
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
Simisunsendable(the aircraft usesRc/RefCellinternally), so it must be touched only from the event-loop thread.advanceblocks on purpose — with stdio there is a single client and nothing else to serve. See D-010 / D-015.
> 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.
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/A320 Systems Twin · GPLv3 · systems model by FlyByWire (vendored, pinned to 13bce4b) · repository
Get started
Operate
Benchmark
Reference