stella-mcp
About
MCP server for creating and manipulating Stella system dynamics models (.stmx files in XMILE format)
Details
- Author
- bradleylab
- Categories
- Other, Developer Tools
Jump to
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
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.
Sign in to leave a review
Use Google, GitHub, or an email account so ratings stay tied to real people.
No reviews posted yet.





