langgraph-spec-toolkit 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. langgraph_spec_toolkit-0.1.0/.github/workflows/ci.yml +35 -0
  2. langgraph_spec_toolkit-0.1.0/.github/workflows/publish.yml +41 -0
  3. langgraph_spec_toolkit-0.1.0/.gitignore +10 -0
  4. langgraph_spec_toolkit-0.1.0/LICENSE +21 -0
  5. langgraph_spec_toolkit-0.1.0/PKG-INFO +370 -0
  6. langgraph_spec_toolkit-0.1.0/README.md +346 -0
  7. langgraph_spec_toolkit-0.1.0/benchmarks/token_usage.py +130 -0
  8. langgraph_spec_toolkit-0.1.0/examples/simple_chatbot/__init__.py +0 -0
  9. langgraph_spec_toolkit-0.1.0/examples/simple_chatbot/graph.py +37 -0
  10. langgraph_spec_toolkit-0.1.0/examples/simple_chatbot/nodes.py +54 -0
  11. langgraph_spec_toolkit-0.1.0/examples/simple_chatbot/spec.yaml +32 -0
  12. langgraph_spec_toolkit-0.1.0/mcp_server/__init__.py +0 -0
  13. langgraph_spec_toolkit-0.1.0/mcp_server/renderer/__init__.py +3 -0
  14. langgraph_spec_toolkit-0.1.0/mcp_server/renderer/render.py +184 -0
  15. langgraph_spec_toolkit-0.1.0/mcp_server/renderer/templates/graph.py.jinja2 +49 -0
  16. langgraph_spec_toolkit-0.1.0/mcp_server/server.py +158 -0
  17. langgraph_spec_toolkit-0.1.0/mcp_server/spec.py +206 -0
  18. langgraph_spec_toolkit-0.1.0/mcp_server/tools/__init__.py +23 -0
  19. langgraph_spec_toolkit-0.1.0/mcp_server/tools/add_edge.py +32 -0
  20. langgraph_spec_toolkit-0.1.0/mcp_server/tools/add_node.py +41 -0
  21. langgraph_spec_toolkit-0.1.0/mcp_server/tools/get_spec.py +19 -0
  22. langgraph_spec_toolkit-0.1.0/mcp_server/tools/init_project.py +60 -0
  23. langgraph_spec_toolkit-0.1.0/mcp_server/tools/remove_edge.py +24 -0
  24. langgraph_spec_toolkit-0.1.0/mcp_server/tools/remove_node.py +39 -0
  25. langgraph_spec_toolkit-0.1.0/mcp_server/tools/render_python.py +38 -0
  26. langgraph_spec_toolkit-0.1.0/mcp_server/tools/set_state_schema.py +14 -0
  27. langgraph_spec_toolkit-0.1.0/mcp_server/tools/validate_graph.py +19 -0
  28. langgraph_spec_toolkit-0.1.0/mcp_server/validator/__init__.py +3 -0
  29. langgraph_spec_toolkit-0.1.0/mcp_server/validator/validate.py +337 -0
  30. langgraph_spec_toolkit-0.1.0/pyproject.toml +57 -0
  31. langgraph_spec_toolkit-0.1.0/skill/SKILL.md +94 -0
  32. langgraph_spec_toolkit-0.1.0/tests/conftest.py +7 -0
  33. langgraph_spec_toolkit-0.1.0/tests/test_benchmarks.py +36 -0
  34. langgraph_spec_toolkit-0.1.0/tests/test_renderer.py +174 -0
  35. langgraph_spec_toolkit-0.1.0/tests/test_server.py +21 -0
  36. langgraph_spec_toolkit-0.1.0/tests/test_spec.py +158 -0
  37. langgraph_spec_toolkit-0.1.0/tests/test_tools.py +273 -0
  38. langgraph_spec_toolkit-0.1.0/tests/test_validator.py +201 -0
  39. langgraph_spec_toolkit-0.1.0/uv.lock +972 -0
@@ -0,0 +1,35 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ lint:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v7.0.1
13
+ - uses: astral-sh/setup-uv@v10.1.0
14
+ with:
15
+ python-version: "3.12"
16
+ enable-cache: true
17
+ - run: uv sync
18
+ - run: uv run ruff check .
19
+
20
+ test:
21
+ runs-on: ubuntu-latest
22
+ strategy:
23
+ fail-fast: false
24
+ matrix:
25
+ python-version: ["3.11", "3.12"]
26
+ steps:
27
+ - uses: actions/checkout@v7.0.1
28
+ - uses: astral-sh/setup-uv@v10.1.0
29
+ with:
30
+ python-version: ${{ matrix.python-version }}
31
+ enable-cache: true
32
+ - run: uv sync
33
+ - run: uv run pytest
34
+ - name: Smoke-test the MCP server starts
35
+ run: uv run python -m mcp_server.server < /dev/null
@@ -0,0 +1,41 @@
1
+ name: Publish to PyPI
2
+
3
+ # Runs when a GitHub Release is published. Uses PyPI's Trusted Publishing
4
+ # (OIDC) — no API token is stored in this repo. One-time setup required on
5
+ # pypi.org before the first release: register this project (or let the
6
+ # first trusted-publish create it) and add a trusted publisher pointing at
7
+ # this repo + the "publish" workflow + the "pypi" environment. See
8
+ # https://docs.pypi.org/trusted-publishers/
9
+
10
+ on:
11
+ release:
12
+ types: [published]
13
+
14
+ jobs:
15
+ build:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v7.0.1
19
+ - uses: astral-sh/setup-uv@v10.1.0
20
+ with:
21
+ python-version: "3.12"
22
+ - run: uv build
23
+ - uses: actions/upload-artifact@v7.0.1
24
+ with:
25
+ name: dist
26
+ path: dist/
27
+
28
+ publish:
29
+ needs: build
30
+ runs-on: ubuntu-latest
31
+ environment:
32
+ name: pypi
33
+ url: https://pypi.org/p/langgraph-spec-toolkit
34
+ permissions:
35
+ id-token: write # required for Trusted Publishing — do not add a token/password
36
+ steps:
37
+ - uses: actions/download-artifact@v8.0.1
38
+ with:
39
+ name: dist
40
+ path: dist/
41
+ - uses: pypa/gh-action-pypi-publish@v1.14.2
@@ -0,0 +1,10 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mkrishna-gs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,370 @@
1
+ Metadata-Version: 2.5
2
+ Name: langgraph-spec-toolkit
3
+ Version: 0.1.0
4
+ Summary: MCP server + Claude skill for building LangGraph projects by editing a structured YAML spec instead of regenerating Python each turn
5
+ Project-URL: Homepage, https://github.com/mkrishna-gs/langgraph-spec-toolkit
6
+ Project-URL: Repository, https://github.com/mkrishna-gs/langgraph-spec-toolkit
7
+ Project-URL: Issues, https://github.com/mkrishna-gs/langgraph-spec-toolkit/issues
8
+ Author-email: Murali Krishna Ganesa Subramanian <44777244+mkrishna-gs@users.noreply.github.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,codegen,langgraph,llm,mcp,model-context-protocol
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Code Generators
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: jinja2>=3.1
21
+ Requires-Dist: mcp>=1.2.0
22
+ Requires-Dist: pyyaml>=6.0
23
+ Description-Content-Type: text/markdown
24
+
25
+ # langgraph-spec-toolkit
26
+
27
+ [![CI](https://github.com/mkrishna-gs/langgraph-spec-toolkit/actions/workflows/ci.yml/badge.svg)](https://github.com/mkrishna-gs/langgraph-spec-toolkit/actions/workflows/ci.yml)
28
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
29
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](pyproject.toml)
30
+ [![Status: v0.1 alpha](https://img.shields.io/badge/status-v0.1%20alpha-orange.svg)](#project-status)
31
+
32
+ **An MCP server + Claude skill for building [LangGraph](https://github.com/langchain-ai/langgraph) projects by editing a structured YAML spec — not by regenerating Python from scratch on every turn.**
33
+
34
+ ```
35
+ edit spec.yaml (via MCP tools) → validate_graph → render_python → graph.py
36
+ ```
37
+
38
+ Graph topology — nodes, edges, state schema, checkpointer — is data, not
39
+ prose. An LLM agent should be able to add a node or rewire an edge with one
40
+ small, targeted tool call, not re-emit 150 lines of Python and hope nothing
41
+ upstream broke. `spec.yaml` is the source of truth; `graph.py` is a
42
+ deterministic, regenerable build artifact you never hand-edit.
43
+
44
+ ## Table of contents
45
+
46
+ - [Why](#why)
47
+ - [Prior art](#prior-art)
48
+ - [Project status](#project-status)
49
+ - [Installation](#installation)
50
+ - [Usage](#usage)
51
+ - [The spec format](#the-spec-format)
52
+ - [MCP tools](#mcp-tools)
53
+ - [Validation](#validation)
54
+ - [Example](#example)
55
+ - [Benchmarks](#benchmarks)
56
+ - [Development](#development)
57
+ - [Releasing](#releasing)
58
+ - [Contributing](#contributing)
59
+ - [License](#license)
60
+
61
+ ## Why
62
+
63
+ - **Token cost.** A full-file rewrite scales with graph size on every edit;
64
+ a spec edit doesn't. Measured with [`benchmarks/token_usage.py`](benchmarks/token_usage.py)
65
+ (`uv run python benchmarks/token_usage.py`, no network access or vendor
66
+ SDK required — see the script for what "token" means here):
67
+
68
+ | Nodes in graph | Full `graph.py` regen (tokens) | One spec edit (tokens) | Ratio |
69
+ |---:|---:|---:|---:|
70
+ | 3 | 168 | 21 | 8.0x |
71
+ | 5 | 204 | 21 | 9.7x |
72
+ | 10 | 294 | 21 | 14.0x |
73
+ | 25 | 564 | 21 | 26.9x |
74
+ | 50 | 1014 | 21 | 48.3x |
75
+ | 100 | 1914 | 21 | 91.1x |
76
+
77
+ A single spec edit stays flat regardless of graph size; a full-file
78
+ regen grows linearly with it. Token counts use a small offline
79
+ approximate tokenizer, not a specific vendor's real BPE tokenizer, so
80
+ the numbers are illustrative rather than exact — the shape of the curve
81
+ (flat vs. linear) is the actual claim, and holds under any reasonable
82
+ way of counting.
83
+ - **Error rate.** Free-form Python regeneration risks silently dropping an
84
+ edge, mistyping a state key, or producing an unreachable node. A
85
+ structured spec can be validated *before* any code is emitted.
86
+ - **Diffability.** `spec.yaml` changes are small, reviewable diffs. A
87
+ regenerated file's diff is often the whole file.
88
+
89
+ ## Prior art
90
+
91
+ [`langgraph-codegen`](https://pypi.org/project/langgraph-codegen/) already
92
+ does DSL → Python codegen for LangGraph and is worth a look. It ships as a
93
+ library/CLI, without an MCP server, a validation pass, diagramming, or a
94
+ skill layer for an LLM to drive it interactively — that's the gap this
95
+ project fills. We use our own spec format rather than adopting its DSL.
96
+
97
+ ## Project status
98
+
99
+ **v0.1 (current, `0.1.0`)** — first cut, functional end-to-end on a single
100
+ flat graph:
101
+
102
+ - Spec schema: state fields, nodes, edges (simple + conditional), checkpointer.
103
+ - MCP tools: `init_project`, `add_node`, `add_edge`, `remove_node`,
104
+ `remove_edge`, `set_state_schema`, `get_spec`, `validate_graph`,
105
+ `render_python`.
106
+ - Validation: unreachable nodes, missing path to `END`, dangling
107
+ conditions/edges, duplicate/typo'd ids, unsafe identifiers in
108
+ `function`/`condition`/state field names/reducers.
109
+ - Deterministic Jinja2 codegen — no LLM in the render path.
110
+ - `pytest` suite covering the spec model, validator, renderer, and every
111
+ MCP tool.
112
+
113
+ Out of scope for v0.1: diagramming, subgraphs, multi-file projects, and a
114
+ `langgraph-codegen`-style DSL importer. This is a young project; expect the
115
+ spec schema and tool signatures to evolve before 1.0.
116
+
117
+ ## Installation
118
+
119
+ Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/) (which provides
120
+ `uvx`).
121
+
122
+ **Via `uvx`** (recommended once a release is published — no clone, no local
123
+ install; `uvx` fetches and runs it on demand):
124
+
125
+ ```json
126
+ {
127
+ "mcpServers": {
128
+ "langgraph-spec-toolkit": {
129
+ "command": "uvx",
130
+ "args": ["langgraph-spec-toolkit"]
131
+ }
132
+ }
133
+ }
134
+ ```
135
+
136
+ **From source** (needed until the first PyPI release lands, or if you're
137
+ developing on the toolkit itself):
138
+
139
+ ```bash
140
+ git clone <this-repo>
141
+ cd langgraph-spec-toolkit
142
+ uv sync
143
+ ```
144
+
145
+ ```json
146
+ {
147
+ "mcpServers": {
148
+ "langgraph-spec-toolkit": {
149
+ "command": "uv",
150
+ "args": ["run", "--directory", "/path/to/langgraph-spec-toolkit", "python", "-m", "mcp_server.server"]
151
+ }
152
+ }
153
+ }
154
+ ```
155
+
156
+ Runtime dependencies are intentionally minimal: `mcp`, `jinja2`, `pyyaml`.
157
+ `render_python`'s *output* imports `langgraph` (and `langchain-core`, if
158
+ your state uses message types) — those are dependencies of the project
159
+ you're generating, not of this toolkit.
160
+
161
+ ## Usage
162
+
163
+ Either config above starts the MCP server (it speaks MCP over stdio) the
164
+ moment your client connects — there's no separate "run the server" step to
165
+ do by hand. If you want to smoke-test it directly:
166
+
167
+ ```bash
168
+ uv run python -m mcp_server.server # from a source checkout
169
+ uvx langgraph-spec-toolkit # once published
170
+ ```
171
+
172
+ Then drive it through the tools below — or point Claude at `skill/SKILL.md`
173
+ and let it drive itself. A typical session:
174
+
175
+ ```
176
+ init_project(project_dir="my_graph", name="my_graph")
177
+ add_node(project_dir="my_graph", id="start", config={"function": "start"})
178
+ add_node(project_dir="my_graph", id="respond", config={"function": "respond"})
179
+ add_edge(project_dir="my_graph", from_="start", to="respond")
180
+ add_edge(project_dir="my_graph", from_="respond", to="END")
181
+ validate_graph(project_dir="my_graph") # -> ok: true
182
+ render_python(project_dir="my_graph") # -> writes my_graph/graph.py
183
+ ```
184
+
185
+ ...then write `start`/`respond` in `my_graph/nodes.py` and you have a
186
+ runnable graph.
187
+
188
+ ## The spec format
189
+
190
+ `spec.yaml`:
191
+
192
+ ```yaml
193
+ name: simple_chatbot
194
+ entry_point: greet
195
+ state:
196
+ - name: messages
197
+ type: list[BaseMessage]
198
+ reducer: add_messages
199
+ default: []
200
+ nodes:
201
+ - id: greet
202
+ type: python
203
+ config:
204
+ function: greet # callable in nodes.py; defaults to the node id
205
+ - id: chatbot
206
+ type: python
207
+ config:
208
+ function: chatbot
209
+ - id: tools
210
+ type: python
211
+ config:
212
+ function: call_tools
213
+ edges:
214
+ - from: greet
215
+ to: chatbot
216
+ - from: chatbot
217
+ condition: route_after_chatbot # router fn in nodes.py
218
+ paths:
219
+ continue: tools
220
+ end: END
221
+ - from: tools
222
+ to: chatbot
223
+ checkpointer:
224
+ type: none # none | memory | sqlite | postgres
225
+ ```
226
+
227
+ Node and router **bodies are not generated** — `render_python` only owns
228
+ topology, state, and wiring. You write the callables in the project's
229
+ `nodes.py`, named to match `config.function` / `condition`. This keeps
230
+ codegen deterministic: the same spec always renders to the same Python, and
231
+ business logic never gets silently rewritten on a regen.
232
+
233
+ `type` on a state field is a raw Python type expression. A handful of
234
+ common symbols — `BaseMessage`, `AnyMessage`, `HumanMessage`, `AIMessage`,
235
+ `SystemMessage`, `ToolMessage`, `ChatMessage`, plus `Any` / `Optional` /
236
+ `Sequence` / `Union` / `Literal` from `typing` — are recognized by name and
237
+ auto-imported in the rendered file. `reducer` similarly recognizes
238
+ `add_messages` and `add` / `operator.add` as built-ins; anything else is
239
+ assumed to be a function you define in `reducers.py`.
240
+
241
+ ## MCP tools
242
+
243
+ | Tool | Purpose |
244
+ |---|---|
245
+ | `init_project(project_dir, name, state_fields?)` | Scaffold `spec.yaml`, `nodes.py`, `__init__.py`. |
246
+ | `add_node(project_dir, id, type?, config?, entry_point?)` | Add/update a node. The first node added becomes `entry_point` automatically. |
247
+ | `add_edge(project_dir, from_, to?, condition?, paths?)` | Add a simple (`to`) or conditional (`condition` + `paths`) edge. |
248
+ | `remove_node(project_dir, id)` | Remove a node; cascades to delete edges touching it. |
249
+ | `remove_edge(project_dir, from_, to?)` | Remove edge(s) from a source, optionally to one target. |
250
+ | `set_state_schema(project_dir, fields)` | Replace the state schema wholesale. |
251
+ | `get_spec(project_dir)` | Read-only fetch of the full current spec. |
252
+ | `validate_graph(project_dir)` | Run static checks; returns `ok` + a list of issues. |
253
+ | `render_python(project_dir, output_path?)` | Emit `graph.py` (default: `<project_dir>/graph.py`). Blocks on validation *errors*. |
254
+
255
+ > **Note:** edges use the parameter name `from_`, not `from` — the latter
256
+ > is a reserved word in Python. It still round-trips through the `from:`
257
+ > key in `spec.yaml`.
258
+
259
+ > **Note:** the mutating tools (`add_node`, `add_edge`, `remove_node`,
260
+ > `remove_edge`, `set_state_schema`) return a compact `summary` (node/edge/
261
+ > state counts, entry point, checkpointer type) rather than the full spec —
262
+ > echoing the whole graph back on every small edit would grow with graph
263
+ > size and quietly erode the token savings this toolkit exists for. Call
264
+ > `get_spec` when you actually need the full picture.
265
+
266
+ ## Validation
267
+
268
+ `validate_graph` checks for:
269
+
270
+ - **Unreachable nodes** — no path from `entry_point`.
271
+ - **Missing path to `END`** — a node that can never terminate the graph.
272
+ - **Dangling conditions** — a conditional edge with no `paths`, or a `paths`
273
+ target that isn't a real node id (or `END`).
274
+ - **State/id typos** — duplicate node ids, duplicate state field names, an
275
+ `entry_point` that doesn't match any node id, an unknown checkpointer type.
276
+ - **Unsafe identifiers** — `config.function`, a conditional edge's
277
+ `condition`, a state field's `name`, and a non-builtin `reducer` are all
278
+ spliced into the generated Python unquoted (e.g. `nodes.<function>`), so
279
+ each must be a valid Python identifier; a state field's `type` must at
280
+ least parse as a Python expression. This is a correctness *and* safety
281
+ check — it's the boundary that keeps a bad spec value from becoming
282
+ arbitrary code in `graph.py`.
283
+
284
+ `render_python` refuses to emit code while validation *errors* are present;
285
+ warnings (like an unreachable node) don't block rendering.
286
+
287
+ ## Example
288
+
289
+ [`examples/simple_chatbot`](examples/simple_chatbot) has a spec with a
290
+ message-reducer state field, a linear edge, and a conditional tool-call
291
+ loop, plus the generated `graph.py` — diff the two to see exactly what
292
+ codegen does. It's been exercised end-to-end against a real `langgraph` +
293
+ `langchain-core` install to confirm the generated wiring executes, not just
294
+ that it parses.
295
+
296
+ ## Benchmarks
297
+
298
+ [`benchmarks/token_usage.py`](benchmarks/token_usage.py) measures the
299
+ token-cost claim in [Why](#why): full `graph.py` regeneration vs. a single
300
+ spec-tool edit, across graph sizes from 3 to 100 nodes.
301
+
302
+ ```bash
303
+ uv run python benchmarks/token_usage.py
304
+ ```
305
+
306
+ No network access or vendor SDK required — see the script's docstring for
307
+ what it counts as a "token" and why.
308
+
309
+ ## Development
310
+
311
+ ```bash
312
+ uv sync
313
+ uv run python -m mcp_server.server # smoke-test the server starts
314
+ uv run pytest # run the test suite
315
+ uv run ruff check . # lint
316
+ ```
317
+
318
+ The test suite (`tests/`) covers `spec.py` (dataclasses, YAML round-trips),
319
+ `validator/` (every check, including the identifier/injection-safety ones),
320
+ `renderer/` (codegen against the committed example, plus each reducer/
321
+ checkpointer variant), every MCP tool's `run()` function, and MCP tool
322
+ registration itself. New tools or spec fields should come with tests in
323
+ the matching file.
324
+
325
+ CI (`.github/workflows/ci.yml`) runs lint and the test suite (on Python
326
+ 3.11 and 3.12) on every push and pull request against `main`.
327
+
328
+ ## Releasing
329
+
330
+ Publishing to PyPI (`.github/workflows/publish.yml`) uses
331
+ [Trusted Publishing](https://docs.pypi.org/trusted-publishers/) — no API
332
+ token is stored in this repo. One-time setup (maintainers only):
333
+
334
+ 1. On [pypi.org](https://pypi.org), add a trusted publisher for this
335
+ project: owner `mkrishna-gs`, repo `langgraph-spec-toolkit`, workflow
336
+ `publish.yml`, environment `pypi`. (If the project doesn't exist on
337
+ PyPI yet, PyPI supports adding a trusted publisher for a
338
+ not-yet-published project name — it claims the name on first publish.)
339
+ 2. In this repo's GitHub settings, create an environment named `pypi`
340
+ (optionally with required reviewers, for an extra manual gate before
341
+ every publish).
342
+
343
+ After that, cutting a release is the whole process:
344
+
345
+ 1. Bump `version` in `pyproject.toml`.
346
+ 2. Tag and push, then publish a GitHub Release from that tag (or use
347
+ `gh release create`).
348
+ 3. `publish.yml` builds the sdist/wheel and publishes them automatically.
349
+
350
+ ## Contributing
351
+
352
+ Issues and pull requests are welcome. For anything beyond a small fix,
353
+ please open an issue first to discuss scope — the spec schema and tool
354
+ signatures are still settling in this pre-1.0 phase, and larger changes are
355
+ easier to land as a shared plan than as a surprise diff.
356
+
357
+ Before opening a PR:
358
+
359
+ 1. `uv sync` and confirm `uv run python -m mcp_server.server` starts cleanly.
360
+ 2. `uv run pytest` and `uv run ruff check .` both pass. New tools or spec
361
+ fields need tests alongside them.
362
+ 3. Keep runtime dependencies to `mcp`, `jinja2`, `pyyaml` — anything else
363
+ belongs in the generated project, not this toolkit (test-only deps go in
364
+ `[dependency-groups.dev]`).
365
+ 4. Keep `render_python` deterministic: no LLM calls, no non-reproducible
366
+ output, in the render path.
367
+
368
+ ## License
369
+
370
+ [MIT](LICENSE)