Code-review agent as an MCP tool#

Build a LangGOAP agent that reviews GitHub pull requests and expose it as an MCP tool so any MCP client (Claude Desktop, Cursor, …) can call it on demand.

The layering: LangGraph already serves MCP at the deployment layer. Every langgraph dev / langgraph deploy deployment serves /mcp routes by default (see the disable_mcp field in the langgraph-cli schema, which defaults to False). LangGOAP does not ship a parallel MCP server; instead it provides a one-command scaffolder that turns a GoapGraph into a langgraph dev-ready deployment directory.

Three steps:

  1. Define a small LangGOAP code-review agent.

  2. Run langgoap deploy-init (or scaffold_deployment) to generate a deployable LangGraph directory.

  3. Run langgraph dev and point an MCP client at the resulting endpoint.

Steps 1 and 2 execute inside this notebook. Step 3 is documented with snippets — langgraph dev is a long-running server and an MCP client is a separate process, so they can’t run in a Jupyter cell.

Backed by tests/integration/test_langgraph_deploy_scaffold.py.

Step 1 — define the code-review agent#

A four-action plan:

Action

Preconditions

Effect

fetch_diff

pr_url set

diff_fetched

analyze_diff

diff_fetched

analysis_complete

run_static_checks

diff_fetched

static_checks_done

draft_review

analysis_complete, static_checks_done

review_drafted

post_review

review_drafted

review_posted

We define the agent as a no-argument factory function. This is the shape langgraph dev expects: a callable that returns a configured graph the entrypoint can compile.

from typing import Any

from langgoap import ActionSpec, GoalSpec, GoapGraph


def build_graph() -> GoapGraph:
    """Build the code-review GoapGraph.

    The actions below stub their effects so the notebook is runnable
    without GitHub credentials.  In production each ``execute`` would
    call the real API (octokit / gh CLI / a LangChain tool) and emit
    its result into world_state.
    """
    actions = [
        ActionSpec(
            name='fetch_diff',
            preconditions={'pr_url': True},
            effects={'diff_fetched': True},
            execute=lambda ws: {
                'diff_fetched': True,
                'diff_text': '# (real impl: fetch from GitHub)',
            },
        ),
        ActionSpec(
            name='analyze_diff',
            preconditions={'diff_fetched': True},
            effects={'analysis_complete': True},
            execute=lambda ws: {'analysis_complete': True},
        ),
        ActionSpec(
            name='run_static_checks',
            preconditions={'diff_fetched': True},
            effects={'static_checks_done': True},
            execute=lambda ws: {'static_checks_done': True},
        ),
        ActionSpec(
            name='draft_review',
            preconditions={
                'analysis_complete': True,
                'static_checks_done': True,
            },
            effects={'review_drafted': True},
            execute=lambda ws: {
                'review_drafted': True,
                'review_text': 'LGTM with minor suggestions.',
            },
        ),
        ActionSpec(
            name='post_review',
            preconditions={'review_drafted': True},
            effects={'review_posted': True},
            execute=lambda ws: {'review_posted': True},
        ),
    ]
    return GoapGraph(actions=actions)


# Quick sanity check inside the notebook.
graph = build_graph().compile()
result = graph.invoke({
    'goal': GoalSpec(conditions={'review_posted': True}),
    'world_state': {'pr_url': True},
})
print(f"status:  {result['status']}")
print(f"steps:   {[h.action_name for h in result['execution_history']]}")
status:  goal_achieved
steps:   ['fetch_diff', 'analyze_diff', 'run_static_checks', 'draft_review', 'post_review']

Step 2 — scaffold the deployment#

scaffold_deployment (or langgoap deploy-init from the CLI) emits the four files langgraph dev needs:

  • langgraph.json — deployment config; points at the entrypoint.

  • graph.py — entrypoint that imports the user factory and binds module-level graph = factory().compile().

  • .env.example — environment variables to fill in.

  • README.md — three-step run instructions including Claude Desktop config.

We scaffold into a temp directory for the demo; in real use you’d scaffold into a sibling directory of your agent module.

import tempfile
from pathlib import Path

from langgoap import scaffold_deployment

tmp_root = Path(tempfile.mkdtemp(prefix='code_review_deploy_'))
deploy_dir = tmp_root / 'code_review_deploy'

scaffold_deployment(
    deploy_dir,
    # Replace ``__main__`` with the real importable module path of
    # ``build_graph`` in your project (e.g. ``my_agent.review:build_graph``).
    graph_factory_module='my_agent.review',
    graph_factory_attr='build_graph',
    agent_name='code_review',
    agent_description=(
        'A LangGOAP agent that reviews GitHub pull requests by '
        'fetching the diff, analyzing it, running static checks, '
        'drafting a review, and posting it back to GitHub.'
    ),
    extra_dependencies=('langchain-openai>=0.3', 'PyGithub>=2.1'),
)

print('Files written:')
for f in sorted(deploy_dir.iterdir()):
    print(f'  {f.relative_to(tmp_root)}')
Files written:
  code_review_deploy/.env.example
  code_review_deploy/README.md
  code_review_deploy/graph.py
  code_review_deploy/langgraph.json

Inspect the generated langgraph.json#

This is the file langgraph dev reads on startup. Note graphs.code_review points at ./graph.py:graph — the symbol the entrypoint binds. MCP routes are enabled by default (no disable_mcp: true here).

print((deploy_dir / 'langgraph.json').read_text())
{
  "python_version": "3.12",
  "graphs": {
    "code_review": "./graph.py:graph"
  },
  "dependencies": [
    "langgoap",
    "langchain-openai>=0.3",
    "PyGithub>=2.1"
  ],
  "env": ".env"
}

Inspect the generated graph.py entrypoint#

print((deploy_dir / 'graph.py').read_text())
"""LangGraph deployment entrypoint.

Generated by ``langgoap deploy-init`` /
``langgoap.integrations.langgraph_deploy.scaffold_deployment``.

``langgraph dev`` and ``langgraph deploy`` look for the
module-level ``graph`` symbol referenced from ``langgraph.json``.
We import the user-supplied factory, call it to construct a
configured :class:`~langgoap.graph.builder.GoapGraph`, then
compile it — the resulting :class:`CompiledStateGraph` is what
LangGraph serves over its routes (including ``/mcp``).
"""

from __future__ import annotations

from my_agent.review import build_graph

# Build + compile.  Add ``checkpointer=...`` to .compile() if your
# agent needs persistence across requests.
_goap_graph = build_graph()
graph = _goap_graph.compile()

Step 3 — run langgraph dev (manual)#

From the deployment directory:

cd code_review_deploy
cp .env.example .env  # fill in OPENAI_API_KEY, GITHUB_TOKEN, etc.
langgraph dev

langgraph dev brings up the LangGraph API server on http://127.0.0.1:2024. MCP routes are at /mcp. Verify with curl:

curl http://127.0.0.1:2024/mcp/tools | jq

You should see code_review listed as an MCP tool with the input schema derived from the graph’s GoapState TypedDict.

Step 4 — point Claude Desktop at the MCP endpoint#

Add to ~/.config/Claude/claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/claude_desktop_config.json):

{
  "mcpServers": {
    "code_review": {
      "transport": {
        "type": "http",
        "url": "http://127.0.0.1:2024/mcp"
      }
    }
  }
}

Restart Claude Desktop. The code_review tool now appears in Claude’s tool list. Ask:

“Review PR #1234 in repo LangGOAP/LangGOAP.”

Claude calls the MCP tool, which hits langgraph dev’s /mcp endpoint, which invokes the LangGOAP code_review graph. The graph plans (fetch_diff analyze run_static_checks draft_review post_review), executes each action, and returns the final world_state — review posted to GitHub.

What just happened (architecturally)#

  • LangGOAP wrote zero MCP code. The transport, tool listing, request lifecycle, and JSON-RPC framing are all LangGraph’s responsibility — exactly the layering CLAUDE.md’s first-class LangChain ecosystem citizen rule prescribes.

  • The user-facing surface from LangGOAP is two things: the GoapGraph builder you already write, and the scaffold_deployment helper that gets you to a deployable directory in one call.

  • Every other MCP feature LangGraph adds in the future (streaming progress events, capability negotiation, multi-tool deployments, authentication) flows through automatically — we don’t have to re-implement them in LangGOAP.

  • For deployments that need richer MCP tool descriptions than the TypedDict-derived defaults (e.g. semantic info from GoalSpec), the right home for that enhancement is upstream in langgraph-api rather than a parallel layer here.

See langgoap/integrations/langgraph_deploy.py for the scaffolder source and tests/integration/test_langgraph_deploy_scaffold.py for the integration-test contract this notebook builds on.

Next steps#

  • Replace the stub execute callables with real GitHub / LangChain tool calls; the scaffolder makes no assumption about action implementations.

  • Pair langgoap deploy-init with langgraph dev for local iteration; switch to langgraph deploy for hosted production deployments. Both serve /mcp by default.

  • See examples/tutorials/deepagents_integration.ipynb for embedding a GoapGraph as a tool inside another agent instead of (or in addition to) deploying it standalone.