# Directory Structure ``` ├── .gitignore ├── .python-version ├── docs │ ├── examples.md │ ├── readme.md │ ├── sys-mcp-load-artifact.png │ ├── sys-mcp-load-prompt.png │ ├── systems-mcp-artifact.png │ └── systems-mcp-prompt.png ├── LICENSE ├── main.py ├── pyproject.toml ├── README.md └── uv.lock ``` # Files -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- ``` 3.13 ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # UV # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. #uv.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/latest/usage/project/#working-with-version-control .pdm.toml .pdm-python .pdm-build/ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ # Ruff stuff: .ruff_cache/ # PyPI configuration file .pypirc ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # systems-mcp [systems-mcp](https://github.com/lethain/systems-mcp) is an MCP server for interacting with the [`lethain:systems`](https://github.com/lethain/systems/) library for systems modeling. It provides two tools: * `run_systems_model` runs the `systems` specification of a systems model. Takes two parameters, the specification and, optionally, the number of rounds to run the model (defaulting to 100). * `load_systems_documentation` loads documentation and examples into the context window. This is useful for priming models to be more helpful at writing systems models. It is intended for running locally in conjunction with Claude Desktop or a similar tool. ## Usage Here's an example of using `systems-mcp` to run and render a model.  Here is the artifact generated from that prompt, including the output from running the systems model.  Finally, here is an example of using the `load_systems_documentation` tool to prime the context window and using it to help generate a systems specification. This is loosely equivalent to including [`lethain:systems/README.md`](https://raw.githubusercontent.com/lethain/systems/refs/heads/master/README.md) in the context window, but also includes a handful of additional examples (see the included files in [./docs/](./docs/).  Then you can render the model as before.  The most interesting piece here is that I've never personally used `systems` to model a social network, but the LLM was able to do a remarkably decent job at generating a specification despite that. ## Installation These instructions describe installation for [Claude Desktop](https://claude.ai/download) on OS X. It should work similarly on other platforms. 1. Install [Claude Desktop](https://claude.ai/download). 2. Clone [systems-mcp](https://github.com/lethain/systems-mcp) into a convenient location, I'm assuming `/Users/will/systems-mcp` 3. Make sure you have `uv` installed, you can [follow these instructions](https://modelcontextprotocol.io/quickstart/server) 4. Go to Cladue Desktop, Setting, Developer, and have it create your MCP config file. Then you want to update your `claude_desktop_config.json`. (Note that you should replace `will` with your user, e.g. the output of `whoami`. cd ~/Library/Application\ Support/Claude/ vi claude_desktop_config.json Then add this section: { "mcpServers": { "systems": { "command": "uv", "args": [ "--directory", "/Users/will/systems-mcp", "run", "main.py" ] } } } 5. Close Claude and reopen it. 6. It should work... ``` -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- ```markdown # Systems `systems` is a set of tools for describing, running and visualizing [systems diagrams](https://lethain.com/systems-thinking/). Installation directions are below, and then get started by [working through the tutorial](./docs/tutorial.md) or reading through the [Jupyter notebook example](./notebooks/hiring.ipynb) example. For a more in-depth look at the system syntax, please read [the syntax specification](./docs/spec.md). ## Quickest start Follow the installation instructions below, then write a system definition such as: Start(10) Start > Middle @ 2 Middle > End You can then evaluate your system (use `--csv` for an importable format): cat tmp.txt | systems-run -r 3 Start Middle End 0 10 0 0 1 8 2 0 2 6 3 1 3 4 4 2 See [the tutorial](./docs/tutorial.md) for more detailed starting information. ## Running in code It's also possible to write code to run your model, rather than rely on the command line tool. For example: from systems.parse import parse def results_for_spec(spec, rounds): model = parse(spec) results = model.run(rounds=rounds) return model, results spec = """Start(10) Start > Middle @ 2 Middle > End""" model, results = results_for_spec(spec, 10) print(results) # outputs: [{'Start': 10, 'Middle': 0, 'End': 0}, {'Start': 8, 'Middle': 2, 'End': 0}, ...] This pattern is particularly useful when running from inside of a Jupyter Notebook, such as the examples in [`lethain/eng-strategy-models`](https://github.com/lethain/eng-strategy-models). ## Installation To install via PyPi: pip install systems To install for local development: git clone https://github.com/lethain/systems.git cd systems python3 -m venv ./env source ./env/bin/activate python setup.py develop Run tests via: python3 -m unittest tests/test_*.py Or run a single test via: python3 tests/test_parse.py TestParse.test_parse_complex_formula Please open an Github issue if you run into any problems! ## Jupyter notebooks Likely the easiest way to iterate on a model is within a Jupyter notebook. See an [example notebook here](./notebooks/hiring.ipynb). To install, follow the installation steps above, and followed by: # install graphviz brew install graphviz # install these additional python packages pip install jupyter pandas matplotlib ## Using the command line tools There are four command line tools that you'll use when creating and debugging systems/ `systems-run` is used to run models: $ cat examples/hiring.txt | systems-run -r 3 PhoneScreens Onsites Offers Hires Employees Departures 0 0 0 0 0 5 0 1 25 0 0 0 5 0 2 25 12 0 0 5 0 3 25 12 6 0 5 0 `systems-viz` is used to visualize models into [Graphviz](https://www.graphviz.org/): $ cat examples/hiring.txt | systems-viz // Parsed digraph { rankdir=LR 0 [label=Candidates] 1 [label=PhoneScreens] // etc, etc, some other stuff } Typically you'll pipe the output of `systems-viz` into `dot`, for example $ cat examples/hiring.txt | systems-viz | dot -Tpng -o tmp.png `systems-format` reads in a model, tokenizes it and formats the tokens into properly formatted results. This is similar to `gofmt`, and could be used for ensuring a consistent house formatting style for your diagrams. (It was primarily implemented to support generating human readable error messages instead of surfacing the tokens to humans when errors arise.) $ cat examples/hiring.txt | systems-fmt [Candidates] > PhoneScreens @ 25 PhoneScreens > Onsites @ 0.5 # etc etc `systems-lex` generates the tokens for a given system file. This is typically most useful when you're extending the lexer to support new types of functionality, but can also be useful for other kinds of debugging: $ cat examples/hiring.txt | systems-lex ('lines', [('line', 1, [('comment', '# wrap with [] to indicate an infinite stock that')]), ('line', 2, [('comment', "# isn't included in each table")]), ('line', 3, [('comment', '# integers are implicitly steady rates')]), ('line', 4, [('infinite_stock', 'Candidates', ('params', [])), ('flow_direction', '>'), ('stock', 'PhoneScreens', ('params', ())), ('flow_delimiter', '@'), ('flow', '', ('params', (('formula', [('whole', '25')]),)))]), ... ] ) ## Error messages The parser will do its best to give you a useful error message. For example, if you're missing delimiters: cat examples/no_delim.txt | systems-run line 1 is missing delimiter '>': "[a] < b @ 25" At worst, it will give you the line number and line that is creating an issue: cat examples/invalid_flow.txt | systems-run line 1 could not be parsed: "a > b @ 0..2" ## Uploading distribution If you are trying to install this on PyPi, the steps are roughly: python3 -m pip install --user --upgrade pip python3 -m pip install --user --upgrade wheel python3 -m pip install --user --upgrade twine python3 setup.py sdist bdist_wheel twine upload --repository-url https://upload.pypi.org/legacy/ dist/* That should more or less work. :) ## Syntax Specification The full the syntax specification is available in [./docs/spec.md](./docs/spec.md), and is replicated here to make this library easier to drive with an LLM. --- This file specifies the language used for describing systems in `systems`. There are three primary kinds of objects to specify: * `stocks` hold values, and * `flows` transition values from one stock to another. * finally, `formula` are used to describe initial and maximum values for stocks, and the magnitude of flows. ## Specifying stocks Stocks are specified on their own line, or implicitly in flow declarations: MyStock This would create a stock named `MyStock` with an initial value of zero and a maximum value of infinity: OtherStock(10) You can also specify maximum values: ThirdStock(0, 10) This would create `ThirdStock` with an initial value of zero, and a maximum value of ten. Going back to `OtherStock` for a moment, you can also use the special literal `inf` to explicitly specify its maximum value: OtherStock(10, inf) This is a more explicit way to specify a stock with an infinite maximum. Generally it's a strange indicator if you're using the `inf` literal directly, and instead you'd use the special syntax for infinite flows: [InfiniteFlow] This `InfiniteFlow` would have initial and maximum values of infinity. Without going too far into the details, initial and maximums can be specified using any legal formula, more on formulas below: Managers(2) Engineers(Managers * 4, Managers * 8) In many cases, though, you'll end up specifying your stocks inline in your flows, as opposed to doing them on their own lines, but the syntax is the same. ## Flows For example, this would have both `a` and `b` would initialize at zero, and both would have infinite maximum values, in addition there would be a flow of one unit per round from `a` to `b` (assuming that `a` is above zero): a > b @ 1 In the above example, `a` has an initial value of zero, so it would never do anything. Most working systems address that problem by starting with an infinite stock: [a] > b @ 5 b > [c] @ 3 In the above, `a` and `c` would be infinite, and `b` would start with a value of zero. You can also solve the empty start problem by specifying non-zero initial values for your stocks: a(10) > b(3) @ 5 b > c(12) @ 1 c > a In this example, `a` is initialized at 10, `b` at 3, and `c` at 12. Note that you don't have to set the value at first reference. It is legal to initialize a value at a later definition of a stock, e.g. this is fine: a(1) > b @ 5 b(2) > c @ 3 c(3) > a @ 1 However, it *is* illegal to initialize the same stock multiple times. a(1) > b(2) @ 1 b(3) > a @ 1 This will throw an error, because you can't initialize `b` twice with different values! ## Rates, Conversions and Leaks Each line specifies two nodes and the link between them. Links are described following the `@` character. The most common type of flow is a `rate`, which is a fixed transfer of values in one stock to another. For example, moving two units per round between `a` and `b`: # these are equivalent a > b @ 2 a > b @ Rate(2) Up to two units will be transfered from `a` to `b` each round. Another common kind of flow is the `conversion` flow, which takes the entire contents of the source stock and multiplies that value against the conversion rate, adding the result to the next flow. # these are equivalent a(10) > b @ 0.5 a(10) > b @ Conversion(0.5) The above would multiple `0.5` against `10` and move `5` units to `b`, with the other `5` units being lost to the conversion rate (e.g. disappearing). A common example of a conversion rate would be the offer acceptance rate in a [hiring funnel](https://lethain.com/hiring-funnel/). The third kind of flow is the `leak`, which combines properties of the `rate` and `conversion` flows. It moves a fixed percentage of the source flow into the destination flow, while leaving the remainder intact. a(10) > b @ Leak(0.2) Considering the difference between the `conversion` and `leak`, if the above were a `conversion`, then the value of `a` after one round would be `0`, but if it's a `leak`, then the value would be `8`. ## Formulas Any flow value, initial value and maximum value can be a formula: Recruiters(3) Engineers(Managers * 4, Managers * 8) [Candidates] > Engineers @ Recruiters * 6 [Candidates] > Managers @ Recruiters * 3 The above system shows that `Engineers` has an initial value of `Managers * 4`, a maximum value of `Managers * 8` and then shows that both `Engineers` and `Managers` grow at multiples of the value of the `Recruiters` stock. This is also a good example of using the `Recruiters` stock as a variable, as it doesn't' actually change over time. ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml [project] name = "systems-mcp" version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.13" dependencies = [ "httpx>=0.28.1", "mcp[cli]>=1.8.0", "systems>=0.1.0", ] ``` -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- ```markdown # Example 1: Basic Stock Flow" # A simple stock and flow example" Start(100) Start > Middle @ 10 Middle > End @ 5""" # Example 2: Hiring Pipeline # A model of a company hiring pipeline", [Candidates] > PhoneScreens @ 25 PhoneScreens > Onsites @ Conversion(0.5) Onsites > Offers @ Conversion(0.5) Offers > Hires @ Conversion(0.7) Hires > Employees @ Conversion(0.9) Employees(5) Employees > Departures @ Leak(0.05) # Example 3: Customer Acquisition and Churn # Model of customer acquisition and retention" [PotentialCustomers] > EngagedCustomers @ 100 # Initial Integration Flow EngagedCustomers > IntegratedCustomers @ Leak(0.5) # Baseline Churn Flow IntegratedCustomers > ChurnedCustomers @ Leak(0.1) # Experience Deprecation Flow IntegratedCustomers > DeprecationImpactedCustomers @ Leak(0.5) # Reintegrated Flow DeprecationImpactedCustomers > IntegratedCustomers @ Leak(0.9) # Deprecation-Influenced Churn DeprecationImpactedCustomers > ChurnedCustomers @ Leak(0.1)""" ``` -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- ```python import os.path import json import sys from typing import Any, Dict, List, Optional, Union from mcp.server.fastmcp import FastMCP # files to include in the call to `load_systems_documentation` DOCUMENTATION_FILES = ("./docs/readme.md", "./docs/examples.md") DOC_CACHE = None # Redirect debug prints to stderr def debug_print(*args, **kwargs): print(*args, file=sys.stderr, **kwargs) # Create MCP server mcp = FastMCP("systems_mcp") @mcp.tool() async def run_systems_model(spec: str, rounds: int = 100) -> str: """Run a systems model and return output of list of dictionaries in JSON. Args: spec: The systems model specification rounds: Number of rounds to run (default: 100) """ try: # Import here to avoid import errors if module is missing from systems.parse import parse debug_print(f"Running systems model for {rounds} rounds") # Parse the model and run it model = parse(spec) results = model.run(rounds=rounds) return json.dumps(results, indent=2, default=str) except Exception as e: debug_print(f"Error running systems model: {e}") return f"<div class='error'>Error running systems model: {str(e)}</div>" @mcp.tool() async def load_systems_documentation() -> str: """Load systems documentation, examples, and specification details to improve the models ability to generate specifications. Returns: Documentation and several examples of systems models """ global DOC_CACHE if DOC_CACHE is None: DOC_CACHE = "" for rel_file_path in DOCUMENTATION_FILES: with open(os.path.abspath(rel_file_path), 'r') as fin: DOC_CACHE += fin.read() + "\n\n" return f"Systems Documentation:\n\n {DOC_CACHE}" if __name__ == "__main__": debug_print("Starting systems-mcp server") mcp.run(transport='stdio') ```