#
tokens: 5317/50000 7/7 files
lines: off (toggle) GitHub
raw markdown copy
# 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.

![Example of prompt for  using systems-mcp](docs/systems-mcp-prompt.png)

Here is the artifact generated from that prompt, including the output from
running the systems model.

![Example of artifact for using the output of systems-mcp](docs/systems-mcp-artifact.png)

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/).

![Example prompt of loading documentation into context window](docs/sys-mcp-load-prompt.png)

Then you can render the model as before.

![Example prompt of rendering the generated model](docs/sys-mcp-load-artifact.png)

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')

```