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.
- langgraph_spec_toolkit-0.1.0/.github/workflows/ci.yml +35 -0
- langgraph_spec_toolkit-0.1.0/.github/workflows/publish.yml +41 -0
- langgraph_spec_toolkit-0.1.0/.gitignore +10 -0
- langgraph_spec_toolkit-0.1.0/LICENSE +21 -0
- langgraph_spec_toolkit-0.1.0/PKG-INFO +370 -0
- langgraph_spec_toolkit-0.1.0/README.md +346 -0
- langgraph_spec_toolkit-0.1.0/benchmarks/token_usage.py +130 -0
- langgraph_spec_toolkit-0.1.0/examples/simple_chatbot/__init__.py +0 -0
- langgraph_spec_toolkit-0.1.0/examples/simple_chatbot/graph.py +37 -0
- langgraph_spec_toolkit-0.1.0/examples/simple_chatbot/nodes.py +54 -0
- langgraph_spec_toolkit-0.1.0/examples/simple_chatbot/spec.yaml +32 -0
- langgraph_spec_toolkit-0.1.0/mcp_server/__init__.py +0 -0
- langgraph_spec_toolkit-0.1.0/mcp_server/renderer/__init__.py +3 -0
- langgraph_spec_toolkit-0.1.0/mcp_server/renderer/render.py +184 -0
- langgraph_spec_toolkit-0.1.0/mcp_server/renderer/templates/graph.py.jinja2 +49 -0
- langgraph_spec_toolkit-0.1.0/mcp_server/server.py +158 -0
- langgraph_spec_toolkit-0.1.0/mcp_server/spec.py +206 -0
- langgraph_spec_toolkit-0.1.0/mcp_server/tools/__init__.py +23 -0
- langgraph_spec_toolkit-0.1.0/mcp_server/tools/add_edge.py +32 -0
- langgraph_spec_toolkit-0.1.0/mcp_server/tools/add_node.py +41 -0
- langgraph_spec_toolkit-0.1.0/mcp_server/tools/get_spec.py +19 -0
- langgraph_spec_toolkit-0.1.0/mcp_server/tools/init_project.py +60 -0
- langgraph_spec_toolkit-0.1.0/mcp_server/tools/remove_edge.py +24 -0
- langgraph_spec_toolkit-0.1.0/mcp_server/tools/remove_node.py +39 -0
- langgraph_spec_toolkit-0.1.0/mcp_server/tools/render_python.py +38 -0
- langgraph_spec_toolkit-0.1.0/mcp_server/tools/set_state_schema.py +14 -0
- langgraph_spec_toolkit-0.1.0/mcp_server/tools/validate_graph.py +19 -0
- langgraph_spec_toolkit-0.1.0/mcp_server/validator/__init__.py +3 -0
- langgraph_spec_toolkit-0.1.0/mcp_server/validator/validate.py +337 -0
- langgraph_spec_toolkit-0.1.0/pyproject.toml +57 -0
- langgraph_spec_toolkit-0.1.0/skill/SKILL.md +94 -0
- langgraph_spec_toolkit-0.1.0/tests/conftest.py +7 -0
- langgraph_spec_toolkit-0.1.0/tests/test_benchmarks.py +36 -0
- langgraph_spec_toolkit-0.1.0/tests/test_renderer.py +174 -0
- langgraph_spec_toolkit-0.1.0/tests/test_server.py +21 -0
- langgraph_spec_toolkit-0.1.0/tests/test_spec.py +158 -0
- langgraph_spec_toolkit-0.1.0/tests/test_tools.py +273 -0
- langgraph_spec_toolkit-0.1.0/tests/test_validator.py +201 -0
- 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,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
|
+
[](https://github.com/mkrishna-gs/langgraph-spec-toolkit/actions/workflows/ci.yml)
|
|
28
|
+
[](LICENSE)
|
|
29
|
+
[](pyproject.toml)
|
|
30
|
+
[](#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)
|