Metadata-Version: 2.5
Name: langgraph-spec-toolkit
Version: 0.2.0
Summary: MCP server + Claude skill for building LangGraph projects by editing a structured YAML spec instead of regenerating Python each turn
Project-URL: Homepage, https://github.com/mkrishna-gs/langgraph-spec-toolkit
Project-URL: Repository, https://github.com/mkrishna-gs/langgraph-spec-toolkit
Project-URL: Issues, https://github.com/mkrishna-gs/langgraph-spec-toolkit/issues
Author-email: Murali Krishna Ganesa Subramanian <44777244+mkrishna-gs@users.noreply.github.com>
License-Expression: MIT
License-File: LICENSE
Keywords: agents,codegen,langgraph,llm,mcp,model-context-protocol
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Code Generators
Requires-Python: >=3.11
Requires-Dist: jinja2>=3.1
Requires-Dist: mcp>=1.2.0
Requires-Dist: pyyaml>=6.0
Description-Content-Type: text/markdown

# langgraph-spec-toolkit

[![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)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](pyproject.toml)
[![Status: v0.2 alpha](https://img.shields.io/badge/status-v0.2%20alpha-orange.svg)](pyproject.toml)

**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.**

```
edit spec.yaml (via MCP tools)  →  validate_graph  →  render_python  →  graph.py
```

Graph topology — nodes, edges, state schema, checkpointer — is data, not
prose. An LLM agent should be able to add a node or rewire an edge with one
small, targeted tool call, not re-emit 150 lines of Python and hope nothing
upstream broke. `spec.yaml` is the source of truth; `graph.py` is a
deterministic, regenerable build artifact you never hand-edit.

## Quick look

![Demo: init_project, apply_changes, validate_graph, and render_python run end to end, producing a deterministic graph.py](.github/assets/demo.gif)

Four tool calls, zero hand-written Python for the graph wiring itself.
(Regenerate with `vhs .github/assets/demo.tape` — see that file for a
`vhs` 0.12.0 bug you may need to work around.)

<details>
<summary>Text transcript, if the GIF doesn't load</summary>

```
$ uv run python .github/assets/demo.py
1) init_project - scaffold spec.yaml, nodes.py

2) apply_changes - nodes + edges wired in one round trip

3) validate_graph - catch problems before any code is emitted

   ok=True  issues=0

4) render_python - deterministic codegen, no LLM involved

   wrote demo_graph/graph.py

$ cat demo_graph/graph.py
"""Auto-generated by langgraph-spec-toolkit — DO NOT EDIT BY HAND.

Regenerate with the `render_python` MCP tool after changing spec.yaml.
Source spec: demo_graph
"""

from langgraph.graph import StateGraph, START, END
from typing import TypedDict
from . import nodes


class GraphState(TypedDict):
    pass


def build_graph():
    workflow = StateGraph(GraphState)

    workflow.add_node('greet', nodes.greet)
    workflow.add_node('respond', nodes.respond)

    workflow.add_edge(START, 'greet')
    workflow.add_edge('greet', 'respond')
    workflow.add_edge('respond', END)

    return workflow.compile()
```

</details>

## Table of contents

- [Quick look](#quick-look)
- [Why](#why)
- [Installation](#installation)
- [Usage](#usage)
- [The spec format](#the-spec-format)
- [MCP tools](#mcp-tools)
- [Validation](#validation)
- [Example](#example)
- [Development](#development)
- [Releasing](#releasing)
- [Contributing](#contributing)
- [License](#license)

## Why

- **Real-world cost.** Measured on a real Claude Code session's `/cost`
  output (not a synthetic estimate), in a fresh session with no prior
  history: building a small 2-node graph from scratch cost **$0.1267**
  hand-writing `graph.py` directly, vs. **$0.1291** through this toolkit's
  MCP tools (using `apply_changes` to wire nodes/edges in one call) —
  roughly at parity for this small, from-scratch case, which is close to
  the toolkit's least favorable scenario since there's no existing
  complexity yet for hand-written regeneration to be expensive.
- **Error rate.** Free-form Python regeneration risks silently dropping an
  edge, mistyping a state key, or producing an unreachable node. A
  structured spec can be validated *before* any code is emitted.
- **Diffability.** `spec.yaml` changes are small, reviewable diffs. A
  regenerated file's diff is often the whole file.

## Installation

Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/) (which provides
`uvx`).

**Via `uvx`** (recommended — no clone, no local install; `uvx` fetches
[`langgraph-spec-toolkit`](https://pypi.org/project/langgraph-spec-toolkit/)
from PyPI and runs it on demand):

```json
{
  "mcpServers": {
    "langgraph-spec-toolkit": {
      "command": "uvx",
      "args": ["langgraph-spec-toolkit"]
    }
  }
}
```

**From source** (if you're developing on the toolkit itself):

```bash
git clone <this-repo>
cd langgraph-spec-toolkit
uv sync
```

```json
{
  "mcpServers": {
    "langgraph-spec-toolkit": {
      "command": "uv",
      "args": ["run", "--directory", "/path/to/langgraph-spec-toolkit", "python", "-m", "mcp_server.server"]
    }
  }
}
```

Runtime dependencies are intentionally minimal: `mcp`, `jinja2`, `pyyaml`.
`render_python`'s *output* imports `langgraph` (and `langchain-core`, if
your state uses message types) — those are dependencies of the project
you're generating, not of this toolkit.

## Usage

Either config above starts the MCP server (it speaks MCP over stdio) the
moment your client connects — there's no separate "run the server" step to
do by hand. If you want to smoke-test it directly:

```bash
uv run python -m mcp_server.server   # from a source checkout
uvx langgraph-spec-toolkit           # from PyPI
```

Then drive it through the tools below — or point Claude at `skill/SKILL.md`
and let it drive itself. A typical session:

```
init_project(project_dir="my_graph", name="my_graph")
apply_changes(project_dir="my_graph", operations=[
    {"op": "add_node", "id": "start"},
    {"op": "add_node", "id": "respond"},
    {"op": "add_edge", "from_": "start", "to": "respond"},
    {"op": "add_edge", "from_": "respond", "to": "END"},
])
validate_graph(project_dir="my_graph")   # -> ok: true
render_python(project_dir="my_graph")    # -> writes my_graph/graph.py
```

...then write `start`/`respond` in `my_graph/nodes.py` and you have a
runnable graph.

## The spec format

`spec.yaml`:

```yaml
name: simple_chatbot
entry_point: greet
state:
  - name: messages
    type: list[BaseMessage]
    reducer: add_messages
    default: []
nodes:
  - id: greet
    type: python
    config:
      function: greet          # callable in nodes.py; defaults to the node id
  - id: chatbot
    type: python
    config:
      function: chatbot
  - id: tools
    type: python
    config:
      function: call_tools
edges:
  - from: greet
    to: chatbot
  - from: chatbot
    condition: route_after_chatbot   # router fn in nodes.py
    paths:
      continue: tools
      end: END
  - from: tools
    to: chatbot
checkpointer:
  type: none                    # none | memory | sqlite | postgres
```

Node and router **bodies are not generated** — `render_python` only owns
topology, state, and wiring. You write the callables in the project's
`nodes.py`, named to match `config.function` / `condition`. This keeps
codegen deterministic: the same spec always renders to the same Python, and
business logic never gets silently rewritten on a regen.

`type` on a state field is a raw Python type expression. A handful of
common symbols — `BaseMessage`, `AnyMessage`, `HumanMessage`, `AIMessage`,
`SystemMessage`, `ToolMessage`, `ChatMessage`, plus `Any` / `Optional` /
`Sequence` / `Union` / `Literal` from `typing` — are recognized by name and
auto-imported in the rendered file. `reducer` similarly recognizes
`add_messages` and `add` / `operator.add` as built-ins; anything else is
assumed to be a function you define in `reducers.py`.

## MCP tools

| Tool | Purpose |
|---|---|
| `init_project(project_dir, name, state_fields?)` | Scaffold `spec.yaml`, `nodes.py`, `__init__.py`. |
| `add_node(project_dir, id, type?, config?, entry_point?)` | Add/update a node. The first node added becomes `entry_point` automatically. |
| `add_edge(project_dir, from_, to?, condition?, paths?)` | Add a simple (`to`) or conditional (`condition` + `paths`) edge. |
| `remove_node(project_dir, id)` | Remove a node; cascades to delete edges touching it. |
| `remove_edge(project_dir, from_, to?)` | Remove edge(s) from a source, optionally to one target. |
| `set_state_schema(project_dir, fields)` | Replace the state schema wholesale. |
| `apply_changes(project_dir, operations)` | Apply several `add_node`/`add_edge`/`remove_node`/`remove_edge`/`set_state_schema` edits in one call — atomic (nothing written if any operation is invalid). |
| `get_spec(project_dir)` | Read-only fetch of the full current spec. |
| `validate_graph(project_dir)` | Run static checks; returns `ok` + a list of issues. |
| `render_python(project_dir, output_path?)` | Emit `graph.py` (default: `<project_dir>/graph.py`). Blocks on validation *errors*. |

> **Note:** edges use the parameter name `from_`, not `from` — the latter
> is a reserved word in Python. It still round-trips through the `from:`
> key in `spec.yaml`.

> **Note:** the mutating tools (`add_node`, `add_edge`, `remove_node`,
> `remove_edge`, `set_state_schema`, `apply_changes`) return a compact
> `summary` (node/edge/state counts, entry point, checkpointer type) rather
> than the full spec — echoing the whole graph back on every small edit
> would grow with graph size and quietly erode the token savings this
> toolkit exists for. Call `get_spec` when you actually need the full
> picture.

> **Prefer `apply_changes` over separate calls whenever wiring more than
> one node/edge at once** (e.g. a whole tool-calling loop) — it's the same
> edit, one round trip instead of several. See [Why](#why) for the
> measured real-world cost. Each operation is a dict with an `"op"` key
> plus that operation's normal arguments, e.g.
> `{"op": "add_node", "id": "tools", "config": {...}}` — see the tool's
> own description for the full list. `entry_point` is set automatically
> (the first node added, or `entry_point: true` on a later `add_node`
> op) — don't add an edge from `"START"` yourself, even though rendered
> `graph.py` contains one; that edge is derived from `entry_point`, not
> wired as a spec edge.

## Validation

`validate_graph` checks for:

- **Unreachable nodes** — no path from `entry_point`.
- **Missing path to `END`** — a node that can never terminate the graph.
- **Dangling conditions** — a conditional edge with no `paths`, or a `paths`
  target that isn't a real node id (or `END`).
- **State/id typos** — duplicate node ids, duplicate state field names, an
  `entry_point` that doesn't match any node id, an unknown checkpointer type.
- **Unsafe identifiers** — `config.function`, a conditional edge's
  `condition`, a state field's `name`, and a non-builtin `reducer` are all
  spliced into the generated Python unquoted (e.g. `nodes.<function>`), so
  each must be a valid Python identifier; a state field's `type` must at
  least parse as a Python expression. This is a correctness *and* safety
  check — it's the boundary that keeps a bad spec value from becoming
  arbitrary code in `graph.py`.

`render_python` refuses to emit code while validation *errors* are present;
warnings (like an unreachable node) don't block rendering.

## Example

[`examples/simple_chatbot`](examples/simple_chatbot) has a spec with a
message-reducer state field, a linear edge, and a conditional tool-call
loop, plus the generated `graph.py` — diff the two to see exactly what
codegen does. It's been exercised end-to-end against a real `langgraph` +
`langchain-core` install to confirm the generated wiring executes, not just
that it parses.

## Development

```bash
uv sync
uv run python -m mcp_server.server   # smoke-test the server starts
uv run pytest                        # run the test suite
uv run ruff check .                  # lint
```

The test suite (`tests/`) covers `spec.py` (dataclasses, YAML round-trips),
`validator/` (every check, including the identifier/injection-safety ones),
`renderer/` (codegen against the committed example, plus each reducer/
checkpointer variant), every MCP tool's `run()` function, and MCP tool
registration itself. New tools or spec fields should come with tests in
the matching file.

CI (`.github/workflows/ci.yml`) runs lint and the test suite (on Python
3.11 and 3.12) on every push and pull request against `main`.

## Releasing

Publishing to PyPI (`.github/workflows/publish.yml`) uses
[Trusted Publishing](https://docs.pypi.org/trusted-publishers/) — no API
token is stored in this repo. One-time setup (maintainers only):

1. On [pypi.org](https://pypi.org), add a trusted publisher for this
   project: owner `mkrishna-gs`, repo `langgraph-spec-toolkit`, workflow
   `publish.yml`, environment `pypi`. (If the project doesn't exist on
   PyPI yet, PyPI supports adding a trusted publisher for a
   not-yet-published project name — it claims the name on first publish.)
2. In this repo's GitHub settings, create an environment named `pypi`
   (optionally with required reviewers, for an extra manual gate before
   every publish).

After that, cutting a release is the whole process:

1. Bump `version` in `pyproject.toml`.
2. Tag and push, then publish a GitHub Release from that tag (or use
   `gh release create`).
3. `publish.yml` builds the sdist/wheel and publishes them automatically.

## Contributing

Issues and pull requests are welcome — see
[`CONTRIBUTING.md`](CONTRIBUTING.md) for the project layout, dev setup, and
the checklist to run through before opening a PR.

## License

[MIT](LICENSE)
