Your graph is useless if only your scripts can reach it. This lesson puts it behind an MCP server and wires it into both drivers, so the agent can retrieve memory on demand instead of stuffing the whole history into its context window.
First, a correction
The widely-shared article this course is built around gives this MCP configuration:
{
"mcpServers": {
"graphiti": {
"command": "uvx",
"args": ["graphiti-mcp"],
"env": {
"NEO4J_URI": "bolt://localhost:7687",
"NEO4J_PASSWORD": "${NEO4J_PASSWORD}"
}
}
}
}
That does not work. There is no graphiti-mcp package on PyPI:
$ curl -s -o /dev/null -w "%{http_code}\n" https://pypi.org/pypi/graphiti-mcp/json
404
graphiti-core exists as a library. The MCP server is not a published package, it lives in the mcp_server/ directory of the getzep/graphiti repository and you run it from a checkout. The verified configuration is in Lesson 11.
The server
labs/mcp_server.py exposes the course graph over MCP. The design choice worth noticing is what it does not expose.
There is no run_query tool. There is no execute_cypher. The four tools are exactly the operations the routing policy sanctions:
| Tool | Purpose |
|---|---|
search_entities |
Resolve a fuzzy phrase to canonical entity names. Call this first. |
get_subgraph |
Retrieve a bounded neighbourhood, optionally as of a date. |
add_knowledge |
Write an episode through the validation gate. |
graph_stats |
Report size and shape. |
An MCP server is a policy surface, not just an API wrapper. If you expose arbitrary query execution, your carefully written routing policy becomes a suggestion the model can route around. If the only retrieval tool is bounded by hops and max_edges, the model cannot send you the whole graph even if it wants to.
Notice the tool docstrings do real work:
@mcp.tool()
def get_subgraph(entities: list[str], hops: int = 1, as_of: str = "", max_edges: int = 60) -> str:
"""Retrieve the smallest relevant subgraph around some entities.
entities: canonical names from search_entities
hops: 1 for direct relationships, 2 for transitive. Never more.
as_of: optional YYYY[-MM[-DD]] to see the graph as it was then.
"""
edges = store.subgraph(entities, hops=min(hops, 2), as_of=as_of or None, max_edges=max_edges)
if not edges:
return "No edges found. Do not invent relationships; report the gap."
return render_context(edges)
The docstring is the model’s instruction manual, so it says “call search_entities first” and “never more than 2 hops.” And the empty case returns an explicit instruction rather than an empty string, because an empty result is precisely when a model is most tempted to fill the silence from its training data.
The min(hops, 2) is the belt to that suspenders. Instructions guide, code enforces.
SDK version gotcha
MCP SDK 2.0 renamed the server class. mcp.server.fastmcp.FastMCP became mcp.server.mcpserver.MCPServer. Most tutorials online still show the 1.x import and fail on a current install with a confusing No module named 'mcp.server.fastmcp'.
The lab handles both:
try:
from mcp.server.mcpserver import MCPServer as _Server # mcp >= 2.0
except ImportError: # pragma: no cover
try:
from mcp.server.fastmcp import FastMCP as _Server # mcp 1.x
except ImportError as exc:
print(f"MCP import failed ({exc}). Install the SDK: pip install mcp", file=sys.stderr)
raise SystemExit(1)
Verify before you wire
Never register an MCP server you haven’t confirmed loads. A broken server usually fails silently at agent startup and you’ll spend an hour wondering why the tools aren’t there.
cd labs && .venv/bin/python verify_mcp.py
Real output:
MCP server loaded. Tools registered:
- search_entities: Find entities in the knowledge graph by name or description.
- get_subgraph: Retrieve the smallest relevant subgraph around some entities.
- add_knowledge: Store a new episode as validated graph memory.
- graph_stats: Report the size and shape of the knowledge graph.
accepted: 2 entities, 1 edges
rejected: 0
Dana (person)
GRAPH EVIDENCE (answer only from these edges; say so if insufficient):
- Dana --works_at--> Fabrikam [2025 .. present] (episode 1)
entities=2, edges=1, episodes=1, open_edges=1, aliases=0
Four tools registered, a write accepted through the gate, and a retrieval that came back cited. Now it’s safe to register.
Driver A: Copilot CLI
cd labs
copilot mcp add graphlab \
--env GRAPHLAB_DB=$PWD/memory.db \
-- $PWD/.venv/bin/python $PWD/mcp_server.py
Confirm:
copilot mcp list
copilot mcp get graphlab
Real output from mcp add:
Added server "graphlab"
graphlab
Type: local
Command: /path/to/labs/.venv/bin/python /path/to/labs/mcp_server.py
Environment:
GRAPHLAB_DB: ***
Tools: * (all)
Source: User
Note the -- separator before the command. Everything after it is the command and its args. Without it, Copilot tries to parse your python path as a URL.
Config sources, in case you want the server scoped to one repo instead of your user:
| Scope | File |
|---|---|
| User | ~/.copilot/mcp-config.json |
| Workspace | .mcp.json or .github/mcp.json |
Now use it:
copilot -p "Use the graphlab tools. Search for Alice, get her 1-hop subgraph \
as of 2025, and tell me where she worked. Cite the edges." \
--model claude-opus-4.8 --allow-all-tools
Remove when you’re done experimenting:
copilot mcp remove graphlab
Driver B: Hermes Agent
Add to ~/.hermes/config.yaml:
mcp_servers:
graphlab:
command: "/absolute/path/to/labs/.venv/bin/python"
args: ["/absolute/path/to/labs/mcp_server.py"]
env:
GRAPHLAB_DB: "/absolute/path/to/labs/memory.db"
timeout: 60
connect_timeout: 30
Restart Hermes. At startup it connects, discovers the tools, and registers them as:
mcp_graphlab_search_entities
mcp_graphlab_get_subgraph
mcp_graphlab_add_knowledge
mcp_graphlab_graph_stats
Those are then injected into every platform toolset, so the graph is available in every conversation without per-session setup. That is the meaningful difference from the Copilot flow: Hermes treats MCP tools as ambient capability rather than something you opt into per invocation.
Three Hermes-specific facts worth internalizing:
- No hot reload. Config change means restart.
- Filtered environment. Only
PATH,HOME,USER,LANG,LC_ALL,TERM,SHELL,TMPDIRandXDG_*are inherited by the subprocess. Every other variable, including any API key, must be named explicitly underenv:. That’s a deliberate guard against leaking your whole shell environment to a third-party MCP server, and it is a good default that other clients don’t have. - Errors are redacted. Credential-shaped patterns in MCP error messages are stripped before reaching the model.
Two kinds of memory, one system
Hermes already has its own memory: durable facts, plus skills as procedural memory. Now it also has your graph. Keeping the boundary clear is worth doing deliberately:
| Agent memory (Hermes) | Graph memory (yours) | |
|---|---|---|
| Holds | Preferences, conventions, stable facts about the user | Entities, relationships, temporal state about a domain |
| Size | Small, curated, always in context | Large, retrieved on demand |
| Written by | The agent, deliberately | The ingestion pipeline, through a gate |
| Queried by | Always present in the prompt | Explicit tool call |
The failure mode is dumping domain facts into agent memory until every prompt drags a knowledge base it mostly doesn’t need. Agent memory is for what must always be true. Graph memory is for what must be retrievable.
Exercises
-
Make the model cite. Ask a question through your driver and require the answer to reference specific edges. Then ask something the graph cannot answer and confirm it reports the gap rather than inventing one. If it invents, strengthen the empty-result string in
get_subgraph. -
Add a
close_facttool.store.invalidate()exists but isn’t exposed. Expose it, and decide deliberately whether an agent should be allowed to expire facts autonomously. Write down your reasoning, it’s a real design decision. -
Scope it to a workspace. Move the config from user scope to
.mcp.jsonin a project directory and confirm the tools appear only there.
Next: measure the thing, because everything above is a hypothesis until you have numbers.
Questions and feedback
Stuck on this lesson, spotted an error, or got it working? Sign in with a GitHub account to ask or comment. Threads live as GitHub Discussions on the course repo, so answers stay findable for the next person.