Skip to main content

langgraph-spec-toolkit

CI License: MIT Python 3.11+ Status: v0.1 alpha

An MCP server + Claude skill for building 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.

Table of contents

Why

  • Token cost. A full-file rewrite scales with graph size on every edit; a spec edit doesn't. Measured with benchmarks/token_usage.py (uv run python benchmarks/token_usage.py, no network access or vendor SDK required — see the script for what "token" means here):

    Nodes in graph Full graph.py regen (tokens) One spec edit (tokens) Ratio
    3 168 21 8.0x
    5 204 21 9.7x
    10 294 21 14.0x
    25 564 21 26.9x
    50 1014 21 48.3x
    100 1914 21 91.1x

    A single spec edit stays flat regardless of graph size; a full-file regen grows linearly with it. Token counts use a small offline approximate tokenizer, not a specific vendor's real BPE tokenizer, so the numbers are illustrative rather than exact — the shape of the curve (flat vs. linear) is the actual claim, and holds under any reasonable way of counting.

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

Prior art

langgraph-codegen already does DSL → Python codegen for LangGraph and is worth a look. It ships as a library/CLI, without an MCP server, a validation pass, diagramming, or a skill layer for an LLM to drive it interactively — that's the gap this project fills. We use our own spec format rather than adopting its DSL.

Project status

v0.1 (current, 0.1.0) — first cut, functional end-to-end on a single flat graph:

  • Spec schema: state fields, nodes, edges (simple + conditional), checkpointer.
  • MCP tools: init_project, add_node, add_edge, remove_node, remove_edge, set_state_schema, get_spec, validate_graph, render_python.
  • Validation: unreachable nodes, missing path to END, dangling conditions/edges, duplicate/typo'd ids, unsafe identifiers in function/condition/state field names/reducers.
  • Deterministic Jinja2 codegen — no LLM in the render path.
  • pytest suite covering the spec model, validator, renderer, and every MCP tool.

Out of scope for v0.1: diagramming, subgraphs, multi-file projects, and a langgraph-codegen-style DSL importer. This is a young project; expect the spec schema and tool signatures to evolve before 1.0.

Installation

Requires Python 3.11+ and uv (which provides uvx).

Via uvx (recommended once a release is published — no clone, no local install; uvx fetches and runs it on demand):

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

From source (needed until the first PyPI release lands, or if you're developing on the toolkit itself):

git clone <this-repo>
cd langgraph-spec-toolkit
uv sync
{
  "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:

uv run python -m mcp_server.server   # from a source checkout
uvx langgraph-spec-toolkit           # once published

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")
add_node(project_dir="my_graph", id="start", config={"function": "start"})
add_node(project_dir="my_graph", id="respond", config={"function": "respond"})
add_edge(project_dir="my_graph", from_="start", to="respond")
add_edge(project_dir="my_graph", 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:

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

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

Benchmarks

benchmarks/token_usage.py measures the token-cost claim in Why: full graph.py regeneration vs. a single spec-tool edit, across graph sizes from 3 to 100 nodes.

uv run python benchmarks/token_usage.py

No network access or vendor SDK required — see the script's docstring for what it counts as a "token" and why.

Development

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 — no API token is stored in this repo. One-time setup (maintainers only):

  1. On 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. For anything beyond a small fix, please open an issue first to discuss scope — the spec schema and tool signatures are still settling in this pre-1.0 phase, and larger changes are easier to land as a shared plan than as a surprise diff.

Before opening a PR:

  1. uv sync and confirm uv run python -m mcp_server.server starts cleanly.
  2. uv run pytest and uv run ruff check . both pass. New tools or spec fields need tests alongside them.
  3. Keep runtime dependencies to mcp, jinja2, pyyaml — anything else belongs in the generated project, not this toolkit (test-only deps go in [dependency-groups.dev]).
  4. Keep render_python deterministic: no LLM calls, no non-reproducible output, in the render path.

License

MIT

Release files for langgraph-spec-toolkit 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for langgraph-spec-toolkit 0.1.0
File Size Uploaded
langgraph_spec_toolkit-0.1.0.tar.gz 87.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for langgraph-spec-toolkit 0.1.0
File Interpreter ABI Platform
langgraph_spec_toolkit-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 113.1 kB

Release files / langgraph_spec_toolkit-0.1.0.tar.gz

Download URL langgraph_spec_toolkit-0.1.0.tar.gz
Size 87.6 kB
Tags Source
SHA-256 checksum
How to use checksums
55061190da4241c4022cb67c89d2bfe1f910bd3aa1883ad937fe367849f95f63
BLAKE2b-256 checksum
How to use checksums
16c721e5a607d383f2b6a75a30b5ba7bf3a2db85d80f9d4fd0e579b93917ca0d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.

Transparency log

Release files / langgraph_spec_toolkit-0.1.0-py3-none-any.whl

Download URL langgraph_spec_toolkit-0.1.0-py3-none-any.whl
Size 25.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
04f08ee78bb03187d86db3e384a6b13b94afc85c1152e13fce4404af4d61e8b7
BLAKE2b-256 checksum
How to use checksums
e05dec1d60c27dbc13c4d07d393c4d9ee5193abc66bad628cbd5a53c6d170908
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.

Transparency log

Release history Release notifications | RSS feed

0.2.0

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page