MCP server for agents
vaft.mcp lets an MCP client (Claude Code, Codex or any other) ask VAFT what it can do and read
small numerical views of the packaged reference data. You bring the agent; VAFT runs locally, in
your own environment, over the stdio transport.
The server is an adapter, not a second API. Each tool calls an existing VAFT function, such as
vaft.help(), the formula and process catalogs, the validation registry, vaft.plot.available_plots,
vaft.plot.dd, vaft.plot.extract or vaft.data.sample_manifest, and returns the answer as
bounded JSON.
Install and register
pip install 'vaft[mcp]'
claude mcp add vaft -- python -m vaft.mcp # Claude Code; `vaft mcp` is the same server
Any client that launches a stdio server can run python -m vaft.mcp the same way. Use the Python
interpreter of the environment where VAFT is installed. import vaft never needs the extra: only
vaft.mcp.server.build_server() imports the MCP SDK.
Do not start the server with the vaft/ package directory as the working directory. Python puts the
working directory first on sys.path, so vaft/mcp/ would shadow the SDK’s own mcp package.
The repository root or any other directory is fine.
Tools
| Tool | Answers from |
|---|---|
get_capabilities, get_capability(topic, item) |
vaft.help(): topics, defaults, sections, entry points |
search_formulas(text, category, limit), describe_formula(name) |
vaft.formula.catalog (units, definitions, references; not the source code) |
search_processes(text, category, limit), describe_process(name) |
vaft.process.catalog |
list_validation_checks(category), describe_validation_check(key) |
vaft.validation.registry |
list_plots(query, domain, subject, view, status, limit), describe_plot(name) |
vaft.plot.available_plots |
get_plot_requirements(name) |
vaft.plot.dd(name): the Data Dictionary paths a plot reads |
extract_plot_data(name, shot, max_points, options) |
vaft.plot.extract on a packaged reference shot |
list_samples, describe_sample(shot) |
vaft.data.available_samples / sample_manifest |
list_boundaries(family), describe_boundary(key) |
vaft.formula.boundaries (metadata only) |
inspect_dataset(shot \| artifact) |
which IDS a dataset holds, with time counts and ranges, plus provenance |
inspect_data_path(path, shot \| artifact, time, tolerance, max_points) |
one Data Dictionary path, read without creating it (vaft.ods_access) |
list_equilibrium_times(shot \| artifact) |
the equilibrium slice times |
get_equilibrium_summary(shot \| artifact, time, tolerance, limit) |
the equilibrium_global summary columns of vaft.database.summary (Ip, q95, β_N, li_3, W_mhd, shape, …) |
list_atlas_tables, describe_atlas_table(name) |
the campaign atlas registry: each lane’s table, schema, rules, caveats and README |
query_atlas_table(name, where, columns, order_by, count_by, limit) |
rows of one atlas table, filtered by value |
extract_plot_data returns the plot’s view model, with labels and units as VAFT stores them. Each
array comes back as its shape, dtype, and finite minimum and maximum over all values, plus a
strided preview. The preview is array[::stride], a transport bound, not a resampling.
max_points is a budget for the whole result, shared across its arrays, so a panel overview with
hundreds of arrays gets a few points per array. A result that would exceed about 50 kB keeps array
statistics only. options takes VAFT’s extraction options only; the tool description lists the
allowed keys.
Every result carries truncated: the count of places something was shortened (a list cut to
limit, a long string, an array preview) and the first 20 of those paths. Shot 39915 ships in the
wheel; the other samples load from a Git checkout.
Tools run one at a time in a worker thread, so a slow extraction does not stall the protocol. The server keeps the JSON-RPC stream on a private copy of standard output and points file descriptor 1 at stderr, so output printed by Python, C or Fortran code VAFT calls cannot corrupt the stream.
Read-only by construction
Every tool is annotated readOnlyHint. None of them writes a file, publishes to the database,
reaches HSDS or the network, runs EFIT, CHEASE or any other solver or pipeline, or executes
caller-supplied code or shell commands. Results never carry credentials: help pages say whether
HSDS is configured, not with what, and the server also scrubs the values of HS_PASSWORD and
HS_API_KEY and replaces the home directory with ~ in every result and error message.
Tool inputs name scientific things: a plot, a formula or
a shot. They never name a data representation. During the DD migration (#1127, #1132, #1135),
the loading code can change behind the same tool schema.
Datasets
A dataset is named by shot or by artifact, never by a storage format:
| Name | Read from | When |
|---|---|---|
shot=39915 |
the packaged sample (vaft.omas.sample.sample_ods) |
always; offline |
artifact="run1/g039915.00319" |
a g-file, OMAS JSON, IMAS netCDF or IMAS HDF5 file or directory under VAFT_ARTIFACT_DIR, through vaft.omas.load |
when VAFT_ARTIFACT_DIR is set |
shot=42929, or any shot with database_source="main" |
vaft.database.load, with your own HSDS configuration |
only when the server runs with VAFT_MCP_DATABASE=1 |
Every result carries the dataset’s provenance: its kind, the shot or the artifact path relative to its directory, the loader, the Data Dictionary version when the data records one, and the VAFT version. Database failures report the exception class only, never the server’s text.
Slices are matched by time, never by index. get_equilibrium_summary(time=...) and
inspect_data_path("equilibrium.time_slice.*.global_quantities.q_95", time=...) take the slice
nearest that time. They refuse when it is further away than tolerance (default: half the slice
spacing), and they report the time they matched.
Atlas tables
The campaign atlas is the directory the lanes write their tables to (~/runs/campaign/atlas on
vestserver). Point VAFT_ATLAS_DIR at it. The server knows each table by name, together with
the lane that owns it, the schema that describes its columns, and the rules for reading it:
| Table | Lane | One row is |
|---|---|---|
state, kinetic_profiles |
K (#1454) | an equilibrium state (shot, time_efit_s, efit_lineage), or one Thomson channel of it |
transport, transport_summary |
T (#1453) | a state and r/a (one TGLF configuration), or one state |
transport_sensitivity, transport_sensitivity_pairs |
T | a surface and a TGLF configuration, or the spread over configurations |
stability, stability_surfaces |
N (#1448) | a state and toroidal mode number, or one rational surface for one solver |
zeff_windows, zeff_slices |
Z (#1486) | a discharge window, or a state |
confinement |
D (#1490) | a magnetics state |
op_space_base |
V (#1456) | a state’s operating-space coordinates |
describe_atlas_table returns each column’s unit and definition from the lane’s own schema, the
row key, the lane’s rules and caveats, its README, and provenance: the file’s sha256 and the build
commit. Read it before comparing rows.
query_atlas_table filters with where clauses ({"column", "op", "value"}; ==, !=, <,
<=, >, >=, in, not_in, isnull, notnull), selects columns, sorts with order_by
("-name" for descending) and counts rows per value with count_by. It does not join tables and
does not aggregate beyond counting. A lane’s “never combine” rules are refusals:
stabilityneedsn_tor ==.stability_surfacesneedsn_tor ==andsolver ==.transport_sensitivityneedstglf_config ==.
Older spellings (efit_label, magnetics-only, electron-kinetic) are accepted and returned in
State key contract v1 spelling (efit_quality, magnetics, electron_kinetic). Tables are
read-only, limited to 50 MB, and every result stays under about 50 kB.
Environment
| Variable | Effect |
|---|---|
VAFT_ATLAS_DIR |
the campaign atlas directory; the atlas tools refuse without it |
VAFT_ARTIFACT_DIR |
the directory artifact= paths are relative to; nothing outside it is read |
VAFT_MCP_DATABASE=1 |
allow database shots (read-only, through your own HSDS configuration) |
claude mcp add vaft \
-e VAFT_ATLAS_DIR=$HOME/runs/campaign/atlas \
-e VAFT_ARTIFACT_DIR=$HOME/runs/artifacts \
-- python -m vaft.mcp
A client configured by file (Claude Desktop, a project .mcp.json) takes the same command:
{
"mcpServers": {
"vaft": {
"command": "/path/to/env/bin/python",
"args": ["-m", "vaft.mcp"],
"env": {"VAFT_ATLAS_DIR": "/path/to/campaign/atlas"}
}
}
}
Demo scenarios
workflow/mcp_demo/run_demo.py replays four questions through a real stdio session and writes
the transcripts. The questions are the equilibrium summary of shot 39915, the Tier A states
ideal-unstable at n = 1, the surfaces with the largest SAT-rule flux spread, and the identified
resistive Z_eff windows. Its README lists the same questions to ask an agent live.