LangGraph + Neo4j Integration
Overview
LangGraph is a framework for building stateful, multi-actor applications with LLMs. Unlike fixed agent architectures, LangGraph lets you define custom workflows as graphs—giving you full control over how your agent reasons, routes, and executes. It provides cyclical graph structures, built-in persistence, human-in-the-loop patterns, and streaming support.
While it seamlessly integrates with the LangChain ecosystem, it can be used entirely standalone without LangChain as a dependency.
Installation:
pip install langgraph langchain-neo4j langchain-mcp-adapters
For the GraphRAG retrieval examples:
pip install langgraph neo4j-graphrag neo4j
Key Features:
-
Customizable architecture: Define exactly how your agent flows—add validation steps, parallel branches, approval gates, or multi-agent coordination
-
Explicit node and edge definitions for agent logic
-
Built-in persistence and human-in-the-loop patterns
-
Streaming support for real-time responses
-
Works standalone or with LangChain integrations
-
GraphRAG retrievers from
neo4j-graphragwired as routed graph nodes -
Hosted Neo4j Aura Agents callable over MCP as a sub-agent
Examples
| Notebook | Description |
|---|---|
Build a company research agent using LangGraph with Neo4j, featuring MCP integration, custom tools, vector search, and graph-based workflow orchestration |
|
Build a routed retrieval graph with |
|
Connect a hosted Neo4j Aura Agent to LangGraph as a sub-agent over MCP: machine-to-machine authentication, token caching, and combining the hosted agent with local tools |
Extension Points
1. Custom Graph Architectures
LangGraph’s core value is architectural flexibility. You define nodes (functions) and edges (transitions) to create any workflow:
from langgraph.graph import END, START, StateGraph
from typing_extensions import Annotated, TypedDict
class MessagesState(TypedDict):
messages: Annotated[list[AnyMessage], operator.add]
llm_calls: int
# Build your custom workflow
agent_builder = StateGraph(MessagesState)
# Add nodes - each is a function that transforms state
agent_builder.add_node("llm_call", llm_call)
agent_builder.add_node("tool_node", tool_node)
# Define the flow with edges
agent_builder.add_edge(START, "llm_call")
agent_builder.add_conditional_edges("llm_call", should_continue, ["tool_node", END])
agent_builder.add_edge("tool_node", "llm_call")
agent = agent_builder.compile()
Example architectures you can build:
-
ReAct loops with tool calling (shown in notebook)
-
Multi-agent systems with handoffs
-
Pipelines with validation and retry logic
-
Human-in-the-loop approval workflows
-
Parallel execution branches that merge results
2. MCP Integration
Connect to MCP servers using langchain-mcp-adapters:
from langchain_mcp_adapters.client import MultiServerMCPClient
cypher_mcp_config = {
"neo4j-database": {
"transport": "stdio",
"command": "uvx",
"args": ["mcp-neo4j-cypher"],
"env": {
"NEO4J_URI": os.environ["NEO4J_URI"],
"NEO4J_USERNAME": os.environ["NEO4J_USERNAME"],
"NEO4J_PASSWORD": os.environ["NEO4J_PASSWORD"],
"NEO4J_DATABASE": os.environ["NEO4J_DATABASE"]
}
}
}
client = MultiServerMCPClient(cypher_mcp_config)
mcp_tools = await client.get_tools()
3. Direct Neo4j Integrations
The langchain-neo4j package provides native integrations:
-
Neo4jGraph: Direct connection for executing Cypher queries within custom tools
-
Neo4jVector: Vector store integration for semantic search with custom retrieval queries
4. Custom Tools
Define custom Neo4j tools using the @tool decorator and combine with MCP tools:
from langchain.tools import tool
from langchain_neo4j import Neo4jGraph
neo4j_graph = Neo4jGraph()
@tool
async def get_investments(company: str) -> str:
"""Returns the investments by a company by name."""
results = neo4j_graph.query("""
MATCH (o:Organization)-[:HAS_INVESTOR]->(i)
WHERE o.name = $company
RETURN i.id as id, i.name as name, head(labels(i)) as type
""", {"company": company})
return json.dumps(results, indent=2)
5. Neo4j Checkpoint Savers
The langgraph-checkpoint-neo4j package provides checkpoint savers for persisting agent state to Neo4j:
-
Neo4jSaver: Synchronous checkpointer for storing conversation history and agent state
-
AsyncNeo4jSaver: Async variant for non-blocking checkpoint operations
from langgraph.checkpoint.neo4j import AsyncNeo4jSaver
async with await AsyncNeo4jSaver.from_conn_string(
uri=NEO4J_URI,
user=NEO4J_USERNAME,
password=NEO4J_PASSWORD,
database=NEO4J_DATABASE
) as checkpointer:
await checkpointer.setup()
agent = agent_builder.compile(checkpointer=checkpointer)
6. GraphRAG Retrievers as Graph Nodes
The neo4j-graphrag package provides retrievers that combine vector search with graph traversal:
-
VectorRetriever: Semantic search over embedded text
-
VectorCypherRetriever: Vector search followed by a
retrieval_querythat traverses outward from each match, so retrieved text arrives with its source article, dates, and related entities -
HybridRetriever / HybridCypherRetriever: Vector and full-text search merged, recovering exact matches on names and tickers that embeddings miss
Each has a different strength, which makes them a natural fit for conditional routing. Instead of leaving the choice to the model, a classification node picks the retriever and a grading node decides whether the result is good enough to answer from:
from neo4j_graphrag.retrievers import HybridCypherRetriever
# `node` and `score` are in scope: continue into the graph from each vector hit.
RETRIEVAL_QUERY = """
WITH node AS chunk, score
MATCH (article:Article)-[:HAS_CHUNK]->(chunk)
OPTIONAL MATCH (article)-[:MENTIONS]->(org:Organization)
RETURN chunk.text AS text, article.title AS title,
collect(DISTINCT org.name)[..5] AS companies, score
"""
graph_retriever = HybridCypherRetriever(
driver=driver,
vector_index_name="news",
fulltext_index_name="news_fulltext",
retrieval_query=RETRIEVAL_QUERY,
embedder=OpenAIEmbeddings(),
)
def graph_search(state: RetrievalState):
results = graph_retriever.search(query_text=state["question"], top_k=8)
return {"documents": [item.content for item in results.items]}
builder.add_conditional_edges(
"route_question", pick_retriever,
["vector_search", "hybrid_search", "graph_search"],
)
# Every retriever feeds the same grading node, which cannot be skipped.
for node in ["vector_search", "hybrid_search", "graph_search"]:
builder.add_edge(node, "grade_context")
builder.add_conditional_edges("grade_context", decide_next, ["graph_search", "generate"])
|
The embedding model must match the model that built the vector index. A mismatch does not raise an error — it silently returns meaningless results. Re-embed a stored chunk and compare against its saved vector to confirm the pairing. |
|
|
7. Hosted Aura Agents as Sub-Agents
An Aura Agent is a managed agent configured and hosted in the Aura Console. It reasons over the graph using its own ontology and tools, and returns an answer rather than rows. Exposed over MCP, it becomes a tool your graph can invoke — so one node delegates to an entire agent while the rest of the graph keeps control of the surrounding flow.
client = MultiServerMCPClient({
"aura-agent": {
"transport": "streamable_http",
"url": os.environ["AURA_AGENT_MCP_URL"],
"headers": {"Authorization": f"Bearer {token}"},
}
})
aura_tools = await client.get_tools()
# The hosted agent is one tool among your local ones.
all_tools = aura_tools + [portfolio_weight]
tools_by_name = {t.name: t for t in all_tools}
model_with_tools = model.bind_tools(all_tools)
async def tool_node(state: MessagesState):
"""Run the requested tools. One of them is an entire agent hosted in Aura."""
results = []
for call in state["messages"][-1].tool_calls:
observation = await tools_by_name[call["name"]].ainvoke(call["args"])
results.append(ToolMessage(content=str(observation), tool_call_id=call["id"]))
return {"messages": results}
|
Graph logic stays with the graph: the system prompt needs no schema and no Cypher, and updating the agent’s ontology in the Console changes behaviour without a code change. LangGraph keeps control of the surrounding flow, which is where you would add validation, approval gates, or additional sub-agents. |
MCP Authentication
Supported Mechanisms:
✅ Environment Variables (STDIO transport) - For local MCP servers like mcp-neo4j-cypher, credentials are passed via the env parameter at spawn time.
✅ HTTP Headers (HTTP/SSE transport) - For remote MCP servers, pass API keys or bearer tokens via the headers parameter.
❌ OAuth 2.0 (in-client) - OAuth2 MCP authentication in-client is not currently supported by the Python SDK.
Machine-to-machine tokens. Because in-client OAuth isn’t supported, endpoints that expect OAuth can still be used by fetching the token yourself and passing it as a header. For the Aura Agent MCP endpoint, exchange client credentials from the Aura Console (Account settings → Client credentials → Aura Agent & MCP) for a bearer token:
token = requests.post(
"https://mcp.neo4j.io/oauth/token",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data={
"grant_type": "client_credentials",
"client_id": os.environ["AURA_MCP_CLIENT_ID"],
"client_secret": os.environ["AURA_MCP_CLIENT_SECRET"],
"audience": "https://agent-mcp.neo4j.io",
},
).json()["access_token"]
|
These are not Aura API keys — those authenticate against |
HTTP Transport Configuration (e.g., for Google Colab):
cypher_mcp_config = {
"neo4j-database": {
"url": "http://localhost:8000/mcp",
"transport": "streamable_http"
}
}