stella-mcp

by bradleylab

Not rated
GitHub

About

MCP server for creating and manipulating Stella system dynamics models (.stmx files in XMILE format)

Details

Author
bradleylab
Categories
Other, Developer Tools

Setup

Install stella-mcp in your MCP client (Claude Desktop, Cursor, Windsurf, and others).

Repository: https://github.com/bradleylab/stella-mcp

Follow the installation instructions in the repository README, then restart your MCP client.

A vendor-neutralModel Context Protocol (MCP)server for creating and manipulatingStellasystem dynamics models. Any compliant MCP client can build, read, validate, and save.stmxfiles in the XMILE format; optional host features still vary.

Stellais a system dynamics modeling tool used for simulating complex systems in fields like ecology, biogeochemistry, economics, and engineering. This MCP server allows AI assistants to:

- Create models from scratch- Build stock-and-flow diagrams programmatically
- Read existing models- Parse and understand .stmx files
- Validate models- Check for errors like undefined variables or missing connections
- Modify models- Add stocks, flows, auxiliaries, and connectors
- Save models- Export valid XMILE files that open in Stella Professional

- Teaching system dynamics modeling
- Rapid prototyping of models through natural language
- Batch creation or modification of models
- Documenting and explaining existing models

git clone https://github.com/bradleylab/stella-mcp.git cd stella-mcp pip install -e .

If you haveuvinstalled, the lowest-friction configuration runs the published package directly:

{ "mcpServers": { "stella": { "command": "uvx", "args": ["stella-mcp"] } } }

Add to yourclaude_desktop_config.json:

{ "mcpServers": { "stella": { "command": "stella-mcp" } } }
{ "mcpServers": { "stella": { "command": "stella-mcp" } } }
{ "mcpServers": { "stella": { "command": "python", "args": ["-m", "stella_mcp.server"], "cwd": "/path/to/stella-mcp" } } }

- On MCP 2026-07-28, callcreate_workspaceand carry the returnedworkspace_idthrough stateful calls. Legacy stdio clients may omit it.
- build_modelwith a stablemodel_idand the full set of stocks, auxiliaries, and flows in one call (connector sync and validation run by default, so the response doubles as an inspection).
- Fix validation errors withupdate_,rename_variable, ordelete_variable.
- Extend incrementally withadd_variables(batch) or the single-add tools.
- simulateto sanity-check behavior (requires thesimextra).
- Save withsave_model.
- read_modelwithcompat_mode="permissive"to inspect warnings.
- Runinspect_modelto understand model structure.
- Usecompat_mode="strict"before final save when round-trip fidelity matters.

- MCP 2026-07-28 clients callcreate_workspaceonce and include its returnedworkspace_idin stateful calls. Modern tool discovery marks that field as required on stateful tools. The ID routes application state; it is not an authorization credential.
- Supported legacy stdio clients may omitworkspace_idand use one process-local compatibility workspace; legacy discovery keeps the field optional.
- Tools accept optionalmodel_idso one workspace can manage multiple models safely.
- create_modelandread_modelset the workspace's currentmodel_idand return it.
- add_flowandadd_auxsupport optionalgraphical_functionpayloads (yptsplus exactly one ofxscaleorxpts).
- add_stock/add_flow/add_auxreject duplicate variable names across variable types;add_connectorrequires both variables to exist.
- set_connector_routingcan target a connector byconnector_uidor byfrom_var+to_var.
- save_modelandget_model_xmlacceptauto_layout(defaulttrue) andresolve_layout_violations(defaultfalse).
- save_model,get_model_xml, andrender_diagramreturn the latest layout viewport, metrics, and warnings in structured content. Their text result names any non-clean layout warning codes.
- read_model,save_model, andget_model_xmlacceptcompat_mode:

- permissive(default): continue with warnings
- strict: fail on compatibility issues

build_modelcreates and populates a model in one call. Items apply in the order stocks → auxs → flows → connectors → modules; the whole batch is all-or-nothing, and on failure the error names the failing item (error.stage+error.index). The same item arrays work on an existing model viaadd_variables.

{ "name": "build_model", "arguments": { "name": "SIR", "model_id": "sir", "sim_specs": {"start": 0, "stop": 100, "dt": 0.125, "time_units": "Days"}, "stocks": [ {"name": "Susceptible", "initial_value": "9999", "units": "people"}, {"name": "Infected", "initial_value": "1", "units": "people"}, {"name": "Recovered", "initial_value": "0", "units": "people"} ], "auxs": [ {"name": "contact_rate", "equation": "6"}, {"name": "infectivity", "equation": "0.25"}, {"name": "recovery_time", "equation": "2", "units": "days"}, {"name": "total_population", "equation": "Susceptible + Infected + Recovered"} ], "flows": [ {"name": "infection", "equation": "Susceptible  contact_rate  infectivity  Infected / total_population", "from_stock": "Susceptible", "to_stock": "Infected"}, {"name": "recovery", "equation": "Infected / recovery_time", "from_stock": "Infected", "to_stock": "Recovered"} ], "modules": [ {"name": "Disease Dynamics", "members": ["Susceptible", "Infected", "Recovered"]} ] } }

Connector sync and validation run by default (disable with"sync_connectors": false/"validate": false); the response includes the full structured model summary, so no follow-upinspect_modelcall is needed.

Create and switch between workspace models:

{"name":"create_model","arguments":{"name":"Population","model_id":"pop_v1"}}
{"name":"create_model","arguments":{"name":"Carbon","model_id":"carbon_v1"}}
{"name":"list_models","arguments":{}}
{"name":"delete_model","arguments":{"model_id":"pop_v1"}}
{"name":"inspect_model","arguments":{"model_id":"sir_baseline","include_validation":true}}
{"name":"list_templates","arguments":{}}
{"name":"list_templates","arguments":{"source":"builtin","query":"epidem","tags":["epidemiology"]}}
{"name":"get_template_info","arguments":{"template_name":"sir"}}
{"name":"load_template","arguments":{"template_name":"sir","model_id":"sir_baseline"}}
{"name":"save_as_template","arguments":{"model_id":"pop_v1","template_name":"my_population_template","description":"Baseline single-stock growth starter","tags":["intro","population"]}}
{"name":"create_module","arguments":{"model_id":"sir_baseline","name":"Disease Dynamics","members":["Susceptible","Infected","Recovered"]}}
{"name":"add_to_module","arguments":{"model_id":"sir_baseline","module_name":"Disease Dynamics","members":["infection","recovery"]}}
{"name":"list_modules","arguments":{"model_id":"sir_baseline"}}
{"name":"remove_from_module","arguments":{"model_id":"sir_baseline","module_name":"Disease Dynamics","members":["recovery"]}}
{"name":"rename_module","arguments":{"model_id":"sir_baseline","module_name":"Disease Dynamics","new_name":"Disease Core"}}
{"name":"delete_module","arguments":{"model_id":"sir_baseline","module_name":"Disease Core"}}
{"name":"rename_variable","arguments":{"model_id":"sir_baseline","old_name":"population_total","new_name":"total_population"}}
{"name":"delete_variable","arguments":{"model_id":"sir_baseline","name":"recovery"}}
{"name":"delete_variable","arguments":{"model_id":"sir_baseline","name":"Susceptible","force":true}}
{"name":"update_flow","arguments":{"model_id":"pop_v1","name":"growth","equation":"Population  growth_rate  stress_modifier"}}

Infer missing connectors from equations:

{"name":"sync_connectors_from_equations","arguments":{"model_id":"pop_v1"}}
{"name":"set_module_view","arguments":{"model_id":"sir_baseline","module_name":"Disease Dynamics","x":420,"y":280,"width":420,"height":240}}
{"name":"set_module_style","arguments":{"model_id":"sir_baseline","module_name":"Disease Dynamics","border_color":"#666666","background":"#FFF7E6","font_color":"#333333","font_size":"10pt","label_side":"top"}}

Auto-place module boxes from current member positions:

{"name":"auto_place_module_boxes","arguments":{"model_id":"sir_baseline","padding":40,"only_missing":true}}
{"name":"add_stock","arguments":{"model_id":"pop_v1","name":"Population","initial_value":"100"}}
{"name":"read_model","arguments":{"filepath":"./external_model.stmx","model_id":"imported","compat_mode":"strict"}}

Preview XML in permissive mode (default) and return compatibility warnings when present:

{"name":"get_model_xml","arguments":{"model_id":"imported","compat_mode":"permissive"}}
{ "name": "add_aux", "arguments": { "model_id": "pop_v1", "name": "lookup_rate", "equation": "GRAPH(Time)", "graphical_function": { "xscale": {"min": 0, "max": 100}, "ypts": [0.1, 0.2, 0.4, 0.6], "type": "continuous" } } }

Invalid graphical function payload (rejected):

{ "name": "add_aux", "arguments": { "name": "bad_lookup", "equation": "GRAPH(Time)", "graphical_function": { "xscale": {"min": 0, "max": 100}, "xpts": [0, 10, 20, 30], "ypts": [0.1, 0.2, 0.4, 0.6] } } }
User: Create a simple exponential growth model with a population starting at 100 and a growth rate of 0.1 per year Claude: [Uses create_model, add_stock, add_aux, add_flow, add_connector, save_model] Creates population_growth.stmx with: - Stock: Population (initial=100) - Aux: growth_rate (0.1) - Flow: growth (Population  growth_rate) into Population
User: Read the carbon cycle model and explain what it does Claude: [Uses read_model, list_variables] This model has 3 stocks (Atmosphere, Land Biota, Soil) and 6 flows representing carbon exchange through photosynthesis, respiration...
User: Create a two-box ocean model with surface and deep nutrients Claude: [Uses create_model, add_stock (x4), add_aux (x8), add_flow (x6), save_model] Creates a model with nutrient cycling between surface and deep ocean including upwelling, downwelling, biological uptake, and remineralization

Therender_diagramtool renders the model as an SVG stock-and-flow diagram — stocks as rectangles, auxiliaries as circles, flows as valved pipes (clouds mark sources/sinks), and dependency connectors as routed polylines. The SVG is returned inline so an agent can inspect the layout, and optionally written to a file you can open in any browser. It runs auto-layout first by default, so a freshly built model renders without manual positioning.

{"name":"render_diagram","arguments":{"model_id":"sir_baseline","filepath":"./sir.svg"}}

The diagram below is the built-insirtemplate rendered byrender_diagram(no manual positioning):

Thesimulatetool runs the current model and returns downsampled time series plus per-variable summaries (initial/final/min/max), closing the build→verify loop without opening Stella. It requires the optionalPySDdependency:

{"name":"simulate","arguments":{"model_id":"pop_v1","overrides":{"growth_rate":0.05},"include":["Population"],"max_points":50}}

- PySD integrates withEuler only— models whosemethodis RK4 simulate with Euler and the response carries a warning. Every PySD-backed response identifies the installed PySD version, actual method, declared method, unsupported-feature preflight, and warnings.
- Arrays, compositional module instances, and additional top-level models are preserved-only in 0.14. They fail before PySD with a structuredunsupported_model_featureerror rather than being silently scalarized or flattened.
- PySD and Stella do not have identical output semantics in every supported scalar case. In the retained Lotka-Volterra fixture, Stella caps an outflow to enforce a non-negative stock while PySD reports the uncapped flow equation. The stock trajectories still reach zero together at the next model time.
- overridesaccepts variable names in display ("growth rate") or underscore (growth_rate) form and replaces the variable with a constant.
- save_results_csvwrites the full-resolution results table with atimecolumn.
- The workspace model is never modified by simulation (the run uses a throwaway copy).

Thecompare_scenariostool answers "what happens under these alternative assumptions?" — it runs several named override sets against a baseline (the unmodified model by default) and reports how each diverges. Also requires thesimextra.

{"name":"compare_scenarios","arguments":{"model_id":"pop_v1","include":["Population"],"scenarios":[{"name":"low growth","overrides":{"growth_rate":0.02}},{"name":"high growth","overrides":{"growth_rate":0.08}}]}}

Each scenario reports its own downsampled series plusdelta_vs_baselineper variable:final_abs,final_pct(percent change of the final value), andmax_abs. Notes:

- Every override name across all scenarios is validatedbeforeany run, so a typo fails fast and atomically — no scenario runs half-applied.
- A scenario whose run produces NaN/inf reports the warning in that scenario'swarningswithout aborting the others;final_pctisnullwhen the baseline final is zero (no divide-by-zero).
- baselineis optional — pass an override set to measure deltas against, or omit it to compare against the unmodified model.
- save_comparison_csvwrites a wide table with one column pervariable__scenario(andvariable__baseline).
- The compiled model is reused across every scenario in one call, so a comparison is roughly as cheap as a single simulation plus one run per scenario.

Thesensitivity_analysistool answers "which parameters actually move the outcome?" — it sweeps each parameter one at a time across a range (holding the others at their baseline) and reports how a single chosen output metric responds. Also requires thesimextra.

{"name":"sensitivity_analysis","arguments":{"model_id":"pop_v1","parameters":[{"name":"growth_rate","start":0.02,"stop":0.08,"steps":7}],"output":{"variable":"Population","metric":"final"}}}

For each parameter it returns the metric at every swept value, arange_sensitivity(the metric's average slope across the swept range), and a baseline-normalizedelasticity(≈ Δoutput% / Δparam%) so parameters can be ranked by influence. Notes:

- One-at-a-time only.modeaccepts"oat"; full-factorial (grid) and Monte-Carlo sampling are reserved for a future release.
- metricis one offinal,max,min,mean, ortime_to_threshold(which needs anoutput.thresholdand reports the first time the series crosses it). max/min/mean cover finite values only; a non-finite or never-crossing run reportsnullfor that point with a warning.
- A parameter spec is eitherstart/stop/steps(evenly spaced,steps≥ 2) or an explicitvalueslist (≥ 2 entries).
- max_runs(default 200) caps the total swept runs; an oversized sweeperrorsrather than silently truncating. OAT runs are a sum across parameters, not a product, so the cap only trips on genuinely large sweeps.
- elasticityisnullwhen it cannot be defined (a non-constant parameter, or a zero baseline metric/parameter);range_sensitivityis still reported.
- save_sweep_csvwrites a longparameter, value, metrictable.
- Like scenario comparison, the model is compiled once and reused across the whole sweep.

Thecalibratetool is the inverse ofsimulate: given an observed time-series, it fits constant parameters so the model reproduces the data. Also requires thesimextra.

{"name":"calibrate","arguments":{"model_id":"pop_v1","observations":{"time":[0,2,4,6,8,10],"targets":{"Population":[100,122,149,182,222,271]}},"parameters":[{"name":"growth_rate","initial":0.05,"min":0,"max":0.3}]}}

It returns the fitted parameters (each with its bounds, anat_boundflag, and a linearizedstd_error), the weighted objective trajectory (initial/finalweighted_sseandweighted_rmse), native-unit error metrics for each target, optimizer status/configuration, and warnings. Notes:

- Only constant auxiliaries and flows are calibratable.Stocks are rejected: PySD's parameter override pins a stock to a constant for the whole run rather than setting its initial value, which would silently flatten a dynamic model. Fitting stock initial conditions is not supported.
- Two optimizers.least_squares(default) is local and fast and reports astd_error;differential_evolutionis global, stochastic,seededfor reproducibility, andrequiresmin/maxbounds on every parameter. Itsmaxitergeneration cap defaults to 100.
- Each parameter'sinitialdefaults to the model's current constant value. Bounds are optional forleast_squares, required fordifferential_evolution.
- std_erroris a linearized approximation, not a posterior: it is the covarianceσ²·(JᵀJ)⁻¹and is reported only when there are more observations than parameters, the Jacobian is well-conditioned, and no parameter sits on a bound; otherwise it isnullwith a warning.differential_evolutionreturnsnull(no Jacobian). Under non-defaultweights, the standard-error interpretation holds only for inverse-σ weights.
- Alignment.The model runs over its native[start, stop]window and the simulation is linearly interpolated onto the observation times. Observation timesoutsidethe window are rejected (no extrapolation). All targets share one strictly-increasing time grid; observations are loaded inline or from acsv_path(first column time, the rest targets).
- Optional per-targetweightsare residual multipliers: the optimizer usesweight
(simulated - observed). Inverse measurement-standard-deviation values give normalized residuals. Because weighted errors may mix units,target_metricsreports an unweighted SSE and RMSE in each target's native units; no aggregate native-unit RMSE is reported.save_fit_csvwrites a longtime, target, observed, fittedtable;return_fit_seriesattaches the best-fit series. The model is compiled once and reused across the whole fit.

Beyond tools, the server exposes MCP-native affordances:

- Tool annotations.Every tool carries hints (readOnlyHint,destructiveHint,idempotentHint) so clients can manage permissions and parallelize read-only calls. Inspection tools (inspect_model,validate_model,list_,get_model_xml) are read-only;delete_are marked destructive.
- Resources.Templates and workspace models are readable as resources:

- stella://templates/{name}— a built-in or user template's.stmx
- stella://workspaces/{workspace_id}/models/{model_id}— an explicit workspace model's current XMILE export
- stella://models/{model_id}— the legacy stdio compatibility workspace only

- Undefined variables- References to variables that don't exist
- Mass balance issues- Stocks without flows, flows referencing non-existent stocks
- Missing connections- Equations using variables without connectors (warning)
- Connector endpoint integrity- Connectors pointing at missing variables (error)
- Orphan flows- Flows not connected to any stock
- Circular dependencies- Infinite loops in auxiliary calculations
- Module integrity- Empty modules (warning) and modules referencing missing members (error)
- Units present- A stock or flow missing units while others define them (warning)
- Units consistency- A flow whose units don't read asstock-units/time-unitwhen every attached stock shares the same units (warning; conservative — stays silent on conversion flows and anything it can't confidently parse)
- Unused auxiliaries- An auxiliary referenced by no equation or connector (warning); stocks and flows are never flagged

- Output files use theXMILE standard
- Compatible withStella Professional 1.9+andStella Architect
- permissiveimport/export preserves supported content and the selected unsupported XML fragments where practical, while returning explicit warnings. Editing supported variables does not guarantee references inside preserved-only fragments are updated.
- strictimport/export rejects arrays, compositional module instances, additional top-level models, and confirmed Stella/XMILE reserved identifiers. Arrays and nested models are not implemented features in 0.14.
- Reserved names such asbetaandgammaare preserved with warnings in permissive mode and rejected in strict mode. The built-in SIR template usestransmission_rateandrecovery_rateso Stella does not rename them on save.
- Auto-layout uses a deterministic directed stock-flow backbone, distinct stock ports, obstacle-aware flow and connector routes, label collision checks, and page-grid sizing from complete visual bounds.
- Authored coordinates, including coordinates supplied through update tools, and locked route points remain fixed. Auto-generated coordinates are recomputed on later exports so incrementally extended models can be ranked again instead of freezing after their first save.
- Locked paths are reserved before unlocked routes, labels are selected in normalized-name order, and imported view-font sizes control label geometry in both analysis and SVG previews.
- Information connectors use direct boundary-to-boundary segments whenever the complete diagram leaves them unobstructed and unshared. Stock-flow pipes stay orthogonal because Stella rewrites diagonal flow segments on save.
- Clean planar benchmark layouts have no glyph, label, or route crossings. A graph that cannot be drawn cleanly is still exported and reports a stablelayout.*warning; zero crossings are not promised for arbitrary non-planar graphs.
- Variable names with spaces are converted to underscores internally
- Parser normalizes imported stock inflow/outflow and connector endpoint references
- Time-step export avoids lossy reciprocal rounding (non-exact reciprocals are exported as plaindt)
- Import/export preserves unknown attrs/elements on supported sections (header, sim_specs, variables, views/model extras) to reduce round-trip data loss
- Compatibility corpus regression tests live intests/fixtures/compat_corpus/. A pinned, attributed subset of SDXorg test-models lives intests/fixtures/external_corpus/; both run offline in CI.
- Maintainer helper:python scripts/sync_compat_corpus_manifest.py --checkvalidates corpus manifest sync

The repository test suite covers the MCP stdio protocol, workspace isolation, model construction, validation, SVG rendering, XMILE import/export, simulation, scenario analysis, sensitivity analysis, calibration, and package installation. Run it from a source checkout with:

uv sync --locked --extra dev --extra sim uv run python -m pytest

Pinned Stella-saved and external XMILE fixtures live undertests/fixtures/and run offline in CI. Generated reports, local planning files, and manual review artifacts are intentionally not committed to the source tree.

No reviews yet — be the first

Sign in to leave a review

Use Google, GitHub, or an email account so ratings stay tied to real people.

Email sign in

No reviews posted yet.