#
tokens: 7183/50000 7/7 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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:
--------------------------------------------------------------------------------

```
1 | 3.13
2 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
  1 | # Byte-compiled / optimized / DLL files
  2 | __pycache__/
  3 | *.py[cod]
  4 | *$py.class
  5 | 
  6 | # C extensions
  7 | *.so
  8 | 
  9 | # Distribution / packaging
 10 | .Python
 11 | build/
 12 | develop-eggs/
 13 | dist/
 14 | downloads/
 15 | eggs/
 16 | .eggs/
 17 | lib/
 18 | lib64/
 19 | parts/
 20 | sdist/
 21 | var/
 22 | wheels/
 23 | share/python-wheels/
 24 | *.egg-info/
 25 | .installed.cfg
 26 | *.egg
 27 | MANIFEST
 28 | 
 29 | # PyInstaller
 30 | #  Usually these files are written by a python script from a template
 31 | #  before PyInstaller builds the exe, so as to inject date/other infos into it.
 32 | *.manifest
 33 | *.spec
 34 | 
 35 | # Installer logs
 36 | pip-log.txt
 37 | pip-delete-this-directory.txt
 38 | 
 39 | # Unit test / coverage reports
 40 | htmlcov/
 41 | .tox/
 42 | .nox/
 43 | .coverage
 44 | .coverage.*
 45 | .cache
 46 | nosetests.xml
 47 | coverage.xml
 48 | *.cover
 49 | *.py,cover
 50 | .hypothesis/
 51 | .pytest_cache/
 52 | cover/
 53 | 
 54 | # Translations
 55 | *.mo
 56 | *.pot
 57 | 
 58 | # Django stuff:
 59 | *.log
 60 | local_settings.py
 61 | db.sqlite3
 62 | db.sqlite3-journal
 63 | 
 64 | # Flask stuff:
 65 | instance/
 66 | .webassets-cache
 67 | 
 68 | # Scrapy stuff:
 69 | .scrapy
 70 | 
 71 | # Sphinx documentation
 72 | docs/_build/
 73 | 
 74 | # PyBuilder
 75 | .pybuilder/
 76 | target/
 77 | 
 78 | # Jupyter Notebook
 79 | .ipynb_checkpoints
 80 | 
 81 | # IPython
 82 | profile_default/
 83 | ipython_config.py
 84 | 
 85 | # pyenv
 86 | #   For a library or package, you might want to ignore these files since the code is
 87 | #   intended to run in multiple environments; otherwise, check them in:
 88 | # .python-version
 89 | 
 90 | # pipenv
 91 | #   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
 92 | #   However, in case of collaboration, if having platform-specific dependencies or dependencies
 93 | #   having no cross-platform support, pipenv may install dependencies that don't work, or not
 94 | #   install all needed dependencies.
 95 | #Pipfile.lock
 96 | 
 97 | # UV
 98 | #   Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
 99 | #   This is especially recommended for binary packages to ensure reproducibility, and is more
100 | #   commonly ignored for libraries.
101 | #uv.lock
102 | 
103 | # poetry
104 | #   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105 | #   This is especially recommended for binary packages to ensure reproducibility, and is more
106 | #   commonly ignored for libraries.
107 | #   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108 | #poetry.lock
109 | 
110 | # pdm
111 | #   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112 | #pdm.lock
113 | #   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114 | #   in version control.
115 | #   https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116 | .pdm.toml
117 | .pdm-python
118 | .pdm-build/
119 | 
120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121 | __pypackages__/
122 | 
123 | # Celery stuff
124 | celerybeat-schedule
125 | celerybeat.pid
126 | 
127 | # SageMath parsed files
128 | *.sage.py
129 | 
130 | # Environments
131 | .env
132 | .venv
133 | env/
134 | venv/
135 | ENV/
136 | env.bak/
137 | venv.bak/
138 | 
139 | # Spyder project settings
140 | .spyderproject
141 | .spyproject
142 | 
143 | # Rope project settings
144 | .ropeproject
145 | 
146 | # mkdocs documentation
147 | /site
148 | 
149 | # mypy
150 | .mypy_cache/
151 | .dmypy.json
152 | dmypy.json
153 | 
154 | # Pyre type checker
155 | .pyre/
156 | 
157 | # pytype static type analyzer
158 | .pytype/
159 | 
160 | # Cython debug symbols
161 | cython_debug/
162 | 
163 | # PyCharm
164 | #  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165 | #  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166 | #  and can be added to the global gitignore or merged into this file.  For a more nuclear
167 | #  option (not recommended) you can uncomment the following to ignore the entire idea folder.
168 | #.idea/
169 | 
170 | # Ruff stuff:
171 | .ruff_cache/
172 | 
173 | # PyPI configuration file
174 | .pypirc
175 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | # systems-mcp
 2 | 
 3 | [systems-mcp](https://github.com/lethain/systems-mcp) is an MCP server for interacting with
 4 | the [`lethain:systems`](https://github.com/lethain/systems/) library for systems modeling.
 5 | 
 6 | It provides two tools:
 7 | 
 8 | * `run_systems_model` runs the `systems` specification of a systems model.
 9 |     Takes two parameters, the specification and, optionally, the number of
10 |     rounds to run the model (defaulting to 100).
11 | * `load_systems_documentation` loads documentation and examples into the context window.
12 |     This is useful for priming models to be more helpful at writing systems models.
13 | 
14 | It is intended for running locally in conjunction with Claude Desktop or a similar tool.
15 | 
16 | ## Usage
17 | 
18 | 
19 | Here's an example of using `systems-mcp` to run and render a model.
20 | 
21 | ![Example of prompt for  using systems-mcp](docs/systems-mcp-prompt.png)
22 | 
23 | Here is the artifact generated from that prompt, including the output from
24 | running the systems model.
25 | 
26 | ![Example of artifact for using the output of systems-mcp](docs/systems-mcp-artifact.png)
27 | 
28 | Finally, here is an example of using the `load_systems_documentation` tool to prime
29 | the context window and using it to help generate a systems specification.
30 | 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,
31 | but also includes a handful of additional examples
32 | (see the included files in [./docs/](./docs/).
33 | 
34 | ![Example prompt of loading documentation into context window](docs/sys-mcp-load-prompt.png)
35 | 
36 | Then you can render the model as before.
37 | 
38 | ![Example prompt of rendering the generated model](docs/sys-mcp-load-artifact.png)
39 | 
40 | The most interesting piece here is that I've never personally used `systems` to model a social network,
41 | but the LLM was able to do a remarkably decent job at generating a specification despite that.
42 | 
43 | 
44 | ## Installation
45 | 
46 | These instructions describe installation for [Claude Desktop](https://claude.ai/download) on OS X.
47 | It should work similarly on other platforms.
48 | 
49 | 1. Install [Claude Desktop](https://claude.ai/download).
50 | 2. Clone [systems-mcp](https://github.com/lethain/systems-mcp) into
51 |     a convenient location, I'm assuming `/Users/will/systems-mcp`
52 | 3. Make sure you have `uv` installed, you can [follow these instructions](https://modelcontextprotocol.io/quickstart/server)
53 | 4. Go to Cladue Desktop, Setting, Developer, and have it create your MCP config file.
54 |     Then you want to update your `claude_desktop_config.json`.
55 |     (Note that you should replace `will` with your user, e.g. the output of `whoami`.
56 | 
57 |         cd  ~/Library/Application\ Support/Claude/
58 |         vi claude_desktop_config.json
59 | 
60 |     Then add this section:
61 | 
62 |         {
63 |           "mcpServers": {
64 |             "systems": {
65 |               "command": "uv",
66 |               "args": [
67 |                 "--directory",
68 |                 "/Users/will/systems-mcp",
69 |                 "run",
70 |                 "main.py"
71 |               ]
72 |             }
73 |           }
74 |         }
75 | 
76 | 5. Close Claude and reopen it.
77 | 6. It should work...
78 | 
79 | 
80 | 
```

--------------------------------------------------------------------------------
/docs/readme.md:
--------------------------------------------------------------------------------

```markdown
  1 | 
  2 | # Systems
  3 | 
  4 | `systems` is a set of tools for describing, running and visualizing
  5 | [systems diagrams](https://lethain.com/systems-thinking/).
  6 | 
  7 | 
  8 | Installation directions are below, and then get started by [working through the tutorial](./docs/tutorial.md)
  9 | or reading through the [Jupyter notebook example](./notebooks/hiring.ipynb) example.
 10 | 
 11 | For a more in-depth look at the system syntax, please read [the syntax specification](./docs/spec.md).
 12 | 
 13 | ## Quickest start
 14 | 
 15 | Follow the installation instructions below, then write a system definition
 16 | such as:
 17 | 
 18 |     Start(10)
 19 |     Start  > Middle @ 2
 20 |     Middle > End
 21 | 
 22 | You can then evaluate your system (use `--csv` for an importable format):
 23 | 
 24 |     cat tmp.txt | systems-run -r 3
 25 | 
 26 |             Start   Middle  End
 27 |     0       10      0       0
 28 |     1       8       2       0
 29 |     2       6       3       1
 30 |     3       4       4       2
 31 | 
 32 | See [the tutorial](./docs/tutorial.md) for more detailed starting information.
 33 | 
 34 | ## Running in code
 35 | 
 36 | It's also possible to write code to run your model, rather than rely on the command line tool.
 37 | For example:
 38 | 
 39 |     from systems.parse import parse
 40 | 
 41 |     def results_for_spec(spec, rounds):
 42 |         model = parse(spec)
 43 |         results = model.run(rounds=rounds)
 44 |         return model, results
 45 | 
 46 |     spec = """Start(10)
 47 |     Start  > Middle @ 2
 48 |     Middle > End"""
 49 |     
 50 |     model, results = results_for_spec(spec, 10)
 51 |     print(results)
 52 |     # outputs: [{'Start': 10, 'Middle': 0, 'End': 0}, {'Start': 8, 'Middle': 2, 'End': 0}, ...]
 53 | 
 54 | This pattern is particularly useful when running from inside of a Jupyter Notebook,
 55 | such as the examples in [`lethain/eng-strategy-models`](https://github.com/lethain/eng-strategy-models).
 56 | 
 57 | 
 58 | ## Installation
 59 | 
 60 | To install via PyPi:
 61 | 
 62 |     pip install systems
 63 | 
 64 | To install for local development:
 65 | 
 66 |     git clone https://github.com/lethain/systems.git
 67 |     cd systems
 68 |     python3 -m venv ./env
 69 |     source ./env/bin/activate
 70 |     python setup.py develop
 71 | 
 72 | Run tests via:
 73 | 
 74 |     python3 -m unittest tests/test_*.py
 75 | 
 76 | Or run a single test via:
 77 | 
 78 |     python3 tests/test_parse.py TestParse.test_parse_complex_formula
 79 | 
 80 | Please open an Github issue if you run into any problems!
 81 | 
 82 | ## Jupyter notebooks
 83 | 
 84 | Likely the easiest way to iterate on a model is within a Jupyter notebook.
 85 | See an [example notebook here](./notebooks/hiring.ipynb).
 86 | To install, follow the installation steps above, and followed by:
 87 | 
 88 |     # install graphviz
 89 |     brew install graphviz
 90 | 
 91 |     # install these additional python packages
 92 |     pip install jupyter pandas matplotlib
 93 | 
 94 | 
 95 | 
 96 | ## Using the command line tools
 97 | 
 98 | There are four command line tools that you'll use when creating and debugging
 99 | systems/
100 | 
101 | `systems-run` is used to run models:
102 | 
103 |     $ cat examples/hiring.txt | systems-run -r 3
104 |     PhoneScreens    Onsites Offers  Hires   Employees       Departures
105 |     0       0               0       0       0       5               0
106 |     1       25              0       0       0       5               0
107 |     2       25              12      0       0       5               0
108 |     3       25              12      6       0       5               0
109 | 
110 | `systems-viz` is used to visualize models into [Graphviz](https://www.graphviz.org/):
111 | 
112 |     $ cat examples/hiring.txt | systems-viz
113 |     // Parsed
114 |     digraph {
115 |       rankdir=LR
116 |       0 [label=Candidates]
117 |       1 [label=PhoneScreens]
118 |       // etc, etc, some other stuff
119 |     }
120 | 
121 | Typically you'll pipe the output of `systems-viz` into `dot`, for example
122 | 
123 |     $ cat examples/hiring.txt | systems-viz | dot -Tpng -o tmp.png
124 | 
125 | `systems-format` reads in a model, tokenizes it and formats the tokens
126 | into properly formatted results. This is similar to `gofmt`, and could
127 | be used for ensuring a consistent house formatting style for your diagrams.
128 | (It was primarily implemented to support generating human readable error
129 | messages instead of surfacing the tokens to humans when errors arise.)
130 | 
131 |     $ cat examples/hiring.txt | systems-fmt
132 |     [Candidates] > PhoneScreens @ 25
133 |     PhoneScreens > Onsites @ 0.5
134 |     # etc etc
135 | 
136 | `systems-lex` generates the tokens for a given system file.
137 | This is typically most useful when you're extending the lexer
138 | to support new types of functionality, but can also be useful
139 | for other kinds of debugging:
140 | 
141 |     $ cat examples/hiring.txt | systems-lex
142 |     ('lines',
143 |        [('line',
144 |          1,
145 |          [('comment', '# wrap with [] to indicate an infinite stock that')]),
146 |         ('line', 2, [('comment', "# isn't included in each table")]),
147 | 	('line', 3, [('comment', '# integers are implicitly steady rates')]),
148 | 	('line',
149 | 	 4,
150 |          [('infinite_stock', 'Candidates', ('params', [])),
151 | 	  ('flow_direction', '>'),
152 |           ('stock', 'PhoneScreens', ('params', ())),
153 |           ('flow_delimiter', '@'),
154 |           ('flow', '', ('params', (('formula', [('whole', '25')]),)))]),
155 | 	...
156 |       ]
157 |     )
158 | 
159 | 
160 | ## Error messages
161 | 
162 | The parser will do its best to give you a useful error message.
163 | For example, if you're missing delimiters:
164 | 
165 |     cat examples/no_delim.txt | systems-run
166 |     line 1 is missing delimiter '>': "[a] < b @ 25"
167 | 
168 | At worst, it will give you the line number and line that is
169 | creating an issue:
170 | 
171 |     cat examples/invalid_flow.txt | systems-run
172 |     line 1 could not be parsed: "a > b @ 0..2"
173 | 
174 | ## Uploading distribution
175 | 
176 | If you are trying to install this on PyPi, the steps are roughly:
177 | 
178 |     python3 -m pip install --user --upgrade pip
179 |     python3 -m pip install --user --upgrade wheel
180 |     python3 -m pip install --user --upgrade twine
181 |     python3 setup.py sdist bdist_wheel
182 |     twine upload --repository-url https://upload.pypi.org/legacy/ dist/*
183 | 
184 | That should more or less work. :)
185 | 
186 | 
187 | 
188 | ## Syntax Specification
189 | 
190 | The full the syntax specification is available in [./docs/spec.md](./docs/spec.md),
191 | and is replicated here to make this library easier to drive with an LLM.
192 | 
193 | ---
194 | 
195 | This file specifies the language used for describing systems in `systems`.
196 | There are three primary kinds of objects to specify:
197 | 
198 | * `stocks` hold values, and
199 | * `flows` transition values from one stock to another.
200 | * finally, `formula` are used to describe initial and maximum values for stocks,
201 |     and the magnitude of flows.
202 | 
203 | 
204 | ## Specifying stocks
205 | 
206 | Stocks are specified on their own line, or implicitly in flow declarations:
207 | 
208 |     MyStock
209 | 
210 | This would create a stock named `MyStock` with an initial value of zero and
211 | a maximum value of infinity:
212 | 
213 |     OtherStock(10)
214 | 
215 | You can also specify maximum values:
216 | 
217 |     ThirdStock(0, 10)
218 | 
219 | This would create `ThirdStock` with an initial value of zero, and a maximum value of ten.
220 | 
221 | Going back to `OtherStock` for a moment, you can also use the special literal `inf`
222 | to explicitly specify its maximum value:
223 | 
224 |     OtherStock(10, inf)
225 | 
226 | This is a more explicit way to specify a stock with an infinite maximum.
227 | Generally it's a strange indicator if you're using the `inf` literal directly,
228 | and instead you'd use the special syntax for infinite flows:
229 | 
230 |     [InfiniteFlow]
231 | 
232 | This `InfiniteFlow` would have initial and maximum values of infinity.
233 | 
234 | Without going too far into the details, initial and maximums can be specified using any
235 | legal formula, more on formulas below:
236 | 
237 |     Managers(2)
238 |     Engineers(Managers * 4, Managers * 8)
239 | 
240 | In many cases, though, you'll end up specifying your stocks inline in your
241 | flows, as opposed to doing them on their own lines, but the syntax
242 | is the same.
243 | 
244 | ## Flows
245 | 
246 | For example, this would have both `a` and `b` would initialize at zero,
247 | and both would have infinite maximum values, in addition there would be
248 | a flow of one unit per round from `a` to `b` (assuming that `a` is above zero):
249 | 
250 |     a > b @ 1
251 | 
252 | In the above example, `a` has an initial value of zero, so it would never
253 | do anything. Most working systems address that problem by starting with
254 | an infinite stock:
255 | 
256 |     [a] >  b  @ 5
257 |      b  > [c] @ 3
258 | 
259 | In the above, `a` and `c` would be infinite, and `b` would start
260 | with a value of zero. You can also solve the empty start problem
261 | by specifying non-zero initial values for your stocks:
262 | 
263 |     a(10) > b(3)  @ 5
264 |     b     > c(12) @ 1
265 |     c     > a
266 | 
267 | In this example, `a` is initialized at 10, `b` at 3, and `c` at 12.
268 | Note that you don't have to set the value at first reference. It is legal
269 | to initialize a value at a later definition of a stock, e.g. this is fine:
270 | 
271 |     a(1) > b @ 5
272 |     b(2) > c @ 3
273 |     c(3) > a @ 1
274 | 
275 | However, it *is* illegal to initialize the same stock multiple times.
276 | 
277 |     a(1) > b(2) @ 1
278 |     b(3) > a    @ 1
279 | 
280 | This will throw an error, because you can't initialize `b` twice with different values!
281 | 
282 | ## Rates, Conversions and Leaks
283 | 
284 | Each line specifies two nodes and the link between them. Links are described
285 | following the `@` character. The most common type of flow is a `rate`, which
286 | is a fixed transfer of values in one stock to another.
287 | 
288 | For example, moving two units per round between `a` and `b`:
289 | 
290 |     # these are equivalent
291 |     a > b @ 2
292 |     a > b @ Rate(2)
293 | 
294 | Up to two units will be transfered from `a` to `b` each round.
295 | 
296 | Another common kind of flow is the `conversion` flow, which takes
297 | the entire contents of the source stock and multiplies that value
298 | against the conversion rate, adding the result to the next flow.
299 | 
300 |     # these are equivalent
301 |     a(10) > b @ 0.5
302 |     a(10) > b @ Conversion(0.5)
303 | 
304 | The above would multiple `0.5` against `10` and move `5` units to `b`,
305 | with the other `5` units being lost to the conversion rate (e.g. disappearing).
306 | A common example of a conversion rate would be the offer acceptance rate
307 | in a [hiring funnel](https://lethain.com/hiring-funnel/).
308 | 
309 | The third kind of flow is the `leak`, which combines properties of the
310 | `rate` and `conversion` flows. It moves a fixed percentage of the source
311 | flow into the destination flow, while leaving the remainder intact.
312 | 
313 |     a(10) > b @ Leak(0.2)
314 | 
315 | Considering the difference between the `conversion` and `leak`, if the above
316 | were a `conversion`, then the value of `a` after one round would  be `0`, but if it's
317 | a `leak`, then the value would be `8`.
318 | 
319 | ## Formulas
320 | 
321 | Any flow value, initial value and maximum value can be a formula:
322 | 
323 |     Recruiters(3)
324 |     Engineers(Managers * 4, Managers * 8)
325 |     [Candidates] > Engineers @ Recruiters * 6
326 |     [Candidates] > Managers  @ Recruiters * 3
327 | 
328 | The above system shows that `Engineers` has an initial value of `Managers * 4`,
329 | a maximum value of `Managers * 8` and then shows that both `Engineers` and `Managers`
330 | grow at multiples of the value of the `Recruiters` stock.
331 | 
332 | This is also a good example of using the `Recruiters` stock as
333 | a variable, as it doesn't' actually change over time.
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [project]
 2 | name = "systems-mcp"
 3 | version = "0.1.0"
 4 | description = "Add your description here"
 5 | readme = "README.md"
 6 | requires-python = ">=3.13"
 7 | dependencies = [
 8 |     "httpx>=0.28.1",
 9 |     "mcp[cli]>=1.8.0",
10 |     "systems>=0.1.0",
11 | ]
12 | 
```

--------------------------------------------------------------------------------
/docs/examples.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Example 1: Basic Stock Flow"
 2 | # A simple stock and flow example"
 3 | 
 4 | Start(100)
 5 | Start > Middle @ 10
 6 | Middle > End @ 5"""
 7 | 
 8 | 
 9 | # Example 2: Hiring Pipeline
10 | # A model of a company hiring pipeline",
11 | 
12 | [Candidates] > PhoneScreens @ 25
13 | PhoneScreens > Onsites @ Conversion(0.5)
14 | Onsites > Offers @ Conversion(0.5)
15 | Offers > Hires @ Conversion(0.7)
16 | Hires > Employees @ Conversion(0.9)
17 | Employees(5)
18 | Employees > Departures @ Leak(0.05)
19 | 
20 | 
21 | # Example 3: Customer Acquisition and Churn
22 | # Model of customer acquisition and retention"
23 | 
24 | [PotentialCustomers] > EngagedCustomers @ 100
25 | # Initial Integration Flow
26 | EngagedCustomers > IntegratedCustomers @ Leak(0.5)
27 | # Baseline Churn Flow
28 | IntegratedCustomers > ChurnedCustomers @ Leak(0.1)
29 | # Experience Deprecation Flow
30 | IntegratedCustomers > DeprecationImpactedCustomers @ Leak(0.5)
31 | # Reintegrated Flow
32 | DeprecationImpactedCustomers > IntegratedCustomers @ Leak(0.9)
33 | # Deprecation-Influenced Churn
34 | DeprecationImpactedCustomers > ChurnedCustomers @ Leak(0.1)"""
35 | 
```

--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------

```python
 1 | import os.path
 2 | import json
 3 | import sys
 4 | from typing import Any, Dict, List, Optional, Union
 5 | from mcp.server.fastmcp import FastMCP
 6 | 
 7 | # files to include in the call to `load_systems_documentation`
 8 | DOCUMENTATION_FILES = ("./docs/readme.md", "./docs/examples.md")
 9 | DOC_CACHE = None
10 | 
11 | 
12 | # Redirect debug prints to stderr
13 | def debug_print(*args, **kwargs):
14 |     print(*args, file=sys.stderr, **kwargs)
15 | 
16 | # Create MCP server
17 | mcp = FastMCP("systems_mcp")
18 | 
19 | @mcp.tool()
20 | async def run_systems_model(spec: str, rounds: int = 100) -> str:
21 |     """Run a systems model and return output of list of dictionaries in JSON.
22 |     
23 |     Args:
24 |         spec: The systems model specification
25 |         rounds: Number of rounds to run (default: 100)
26 |     """
27 |     try:
28 |         # Import here to avoid import errors if module is missing
29 |         from systems.parse import parse
30 |         
31 |         debug_print(f"Running systems model for {rounds} rounds")
32 |         
33 |         # Parse the model and run it
34 |         model = parse(spec)
35 |         results = model.run(rounds=rounds)
36 |         return json.dumps(results, indent=2, default=str)
37 |     except Exception as e:
38 |         debug_print(f"Error running systems model: {e}")
39 |         return f"<div class='error'>Error running systems model: {str(e)}</div>"
40 | 
41 | 
42 | @mcp.tool()
43 | async def load_systems_documentation() -> str:
44 |     """Load systems documentation, examples, and specification details to improve
45 |     the models ability to generate specifications.
46 |     
47 |     Returns:
48 |         Documentation and several examples of systems models
49 |     """
50 |     global DOC_CACHE
51 |     if DOC_CACHE is None:
52 |         DOC_CACHE = ""
53 |         for rel_file_path in DOCUMENTATION_FILES:
54 |             with open(os.path.abspath(rel_file_path), 'r') as fin:
55 |                 DOC_CACHE += fin.read() + "\n\n"
56 | 
57 |     return f"Systems Documentation:\n\n {DOC_CACHE}"
58 | 
59 | 
60 | if __name__ == "__main__":
61 |     debug_print("Starting systems-mcp server")
62 |     mcp.run(transport='stdio')
63 | 
```