Architecture
The core constraint
Kratos cannot run inside the MCP server process, for three reasons:
- stdout is sacred. MCP over stdio uses stdout for JSON-RPC framing, and
import KratosMultiphysicsprints a multi-line ASCII banner to stdout. One import would corrupt the protocol stream. - Crashes. Kratos is a C++ core; a solver bug or bad input can abort the whole process (segfault). The server must survive that.
- Environment. A build-tree Kratos needs
PYTHONPATHandLD_LIBRARY_PATHset before the interpreter starts — too late for a process that is already running.
So the server process never imports Kratos. Two subprocess mechanisms cover all Kratos access:
┌──────────────────────────────┐
│ MCP client (Claude, ...) │
└──────────────┬───────────────┘
│ stdio JSON-RPC
┌──────────────▼───────────────┐
│ kratos-mcp server (FastMCP) │ never imports Kratos
│ tools/ · mdpa.py · jobs.py │
└─────┬───────────────────┬────┘
│ bridge.run_op() │ jobs.start()
│ (sync, seconds) │ (detached, minutes-hours)
┌─────▼─────────┐ ┌─────▼─────────┐
│ worker.py │ │ runner.py │ both import Kratos,
│ introspection│ │ AnalysisStage│ env vars injected
│ validation │ │ .Run() │
└───────────────┘ └───────────────┘The same isolation pattern covers pyvista: render_worker.py (spawned by the results_render/results_animate tools) is the only code that imports VTK, because VTK's OpenGL setup can abort the process on headless or misconfigured systems. It doesn't need the Kratos environment — just the server's own interpreter — so it is a plain subprocess without the bridge's env injection or caching.
Getting Kratos itself
kratos_env.resolve() finds Kratos in one of three ways, in order: an explicit KRATOS_PYTHONPATH/KRATOS_LIBS override, a KRATOS_ROOT checkout with a compiled bin/Release, or a pip-installedKratosMultiphysics importable by the server's own interpreter — probed in a subprocess so a missing package fails safely instead of raising in the server process. That third path is what kratos_env.pip_install() and the kratos_install tool populate: they run pip install (never Kratos itself) directly in the server process — safe, since pip does not import Kratos — installing the official KratosMultiphysics / Kratos<AppName> / KratosMultiphysics-all wheels (Linux/Windows x86_64 only). Once installed, the very next resolve() call picks it up automatically; no restart needed. A local build always takes priority when both are present.
The bridge (short operations)
bridge.run_op(op, args) spawns python -m kratos_mcp.worker with the Kratos environment injected and a JSON request file. The worker writes its result to a result file — never stdout — so banners and solver chatter cannot corrupt it; stdout/stderr are captured and attached to error messages.
Ops: check, list_applications, list_variables, has_constitutive_laws, get_solver_defaults, validate_parameters, read_mdpa_deep.
Because each spawn costs one to a few seconds, results of build-dependent ops are cached in ~/.kratos-mcp/cache/, keyed by (op, args, build fingerprint) — editing or rebuilding Kratos invalidates the cache automatically.
Jobs (simulations)
jobs.start() launches python -m kratos_mcp.runner --case-dir ... fully detached (own session), with everything persisted under ~/.kratos-mcp/jobs/<job_id>/:
20260712-101530-a1b2c3/
├── meta.json # state machine: queued → running → succeeded|failed|cancelled
└── stdout.log # combined solver output- Status is recomputed from the process return code, or — after a server restart, when the child handle is gone — from pid liveness plus the AnalysisStage end banner in the log.
- Progress (
current_step,current_time) is parsed from theSTEP:/TIME:lines AnalysisStage prints each step. - Cancel sends SIGTERM to the job's process group and escalates to SIGKILL after a grace period.
The runner picks the analysis class from the analysis_stage key in ProjectParameters.json (the convention used by Kratos itself and by all our templates), from an explicit analysis_type/analysis_class argument, or by inferring it from solver_type.
Hybrid introspection
What the tools report comes from two sources:
- Runtime (authoritative for your build): version, compiled applications, variables, constitutive-law existence, solver default parameters — via the bridge.
- Source parsing: element/condition/constitutive-law catalogs are parsed from
KRATOS_REGISTER_*macros in the C++ sources, because Kratos has no runtime listing for them. Entries are flaggedcompiled: true/falseby cross-referencing the compiled application list.
Pure-Python MDPA layer
mdpa.py parses, writes, validates and generates .mdpa meshes without Kratos, so mesh tools work even where no build is available and unit tests run anywhere. mdpa_validate(deep=true) additionally round-trips the file through the real ModelPartIO in a worker.
Templates as data
Case templates live in src/kratos_mcp/templates/ as JSON files with markers plus a registry.json describing defaults, required applications and solver modules. Substitution is typed: a quoted "" becomes the JSON encoding of the value (numbers stay numbers, arrays stay arrays); a bare inside a longer string is textual.