Remember Yourself: A Personal Knowledge Graph Your AI Can Query
TL;DR: Your AI forgets who you are between sessions. The fix: describe your career once as a small knowledge graph, plain YAML files where every fact carries a source and a privacy tier, served by about 90 lines of Python over MCP. Every AI tool you use can then query who you are instead of being told. Full code and setup below. Total cost: $0.
Every new AI conversation starts with amnesia. The model that helped you yesterday knows nothing today, so you paste your bio again, re-explain your stack again, describe your projects again. The usual fix is a big instruction file loaded into every session: a wall of text about you, most of it irrelevant to any given question, all of it consuming context whether it is needed or not.
There is a better fix, and it takes an afternoon: your career as a knowledge graph, served to any AI tool through the Model Context Protocol (MCP). Plain YAML files, about 90 lines of Python, and every fact carries a source. Ask any of my AI sessions "what do you know about me?" and instead of guessing, it queries the graph.
This is the personal-scale version of an argument I made in Metadata-First Knowledge Graphs: most questions about a person, like most questions about an organization, are relationship questions. Relationships belong in a graph, not in a pile of text. I spend my days building that architecture at enterprise scale; this article is the same thesis small enough to fork.
What a Graph Actually Is
"Knowledge graph" sounds heavier than it is. There are only two pieces:
- Nodes are the nouns: you, an employer, a project, a technology, an article.
- Edges are the verbs that connect them:
worked-at,built-at,uses,published-as.
Both can carry details. A worked-at edge holds the role and the dates; a project node holds facts, each with its source. Here is a real slice of my graph:
flowchart LR
SU(["Sebastian · person"])
AMZ["Amazon · employer"]
TBK["Transbank · employer"]
KG["Enterprise Knowledge Graph · project"]
NEP["Amazon Neptune · technology"]
ART["Metadata-First Knowledge Graphs · article"]
SU -- worked-at --> AMZ
SU -- worked-at --> TBK
KG -- built-at --> AMZ
KG -- uses --> NEP
ART -- about --> KG
The contrast that makes graphs click: in a spreadsheet or a relational database, relationships do not exist as data. They are reconstructed at query time, with joins and lookups, from values that happen to match. A graph stores the relationship itself, so "how is X connected to Y?" is not a reconstruction, it is a walk along edges that are already there. Databases like Amazon Neptune exist because that trick pays off enormously at enterprise scale; a career is small enough that 90 lines of Python and an in-memory graph get you the same power for free.
And for a person, almost everything interesting is a relationship: where you built something, what you built it with, what you wrote about it, what it achieved.
Why a Graph Beats a Bio
Questions about you are traversals. "What did you build at Amazon?" is not a search problem, it is one hop along built-at edges. "Have you used Lambda in production?" is a hop along uses. A bio makes the model infer these connections from prose; a graph makes them lookups.
The token economics invert. With an instruction file, every session pays the full cost of your life story up front. With a graph, the assistant pulls the two facts it actually needs, when it needs them. My entire public graph is 54 entities and 82 edges, and a typical query returns a few hundred tokens.
Provenance makes it trustworthy. Every fact in my graph records where it came from: a public URL, a private repo, a document, or my own recollection (the weakest tier, used sparingly). When the AI writes something about me, it can cite the source instead of hallucinating a better version of my career.
Sensitivity makes it safe. Every fact also carries a tier: public, private-repo, or private. The server filters by audience, so the same graph can serve my own sessions everything while a public deployment can only ever see what is already published.
Files are the source of truth; the engine is disposable. The graph is YAML on disk: versionable, diffable, editable in any text editor, portable to any future engine. The Python below loads it into networkx in memory, but nothing about the data depends on that choice.
The Schema
One entity, one YAML document. This is a real entry from my graph:
id: project-nasa-apod-gallery # unique, kebab-case, type-prefixed
type: project # person | employer | project | article |
# technology | metric | story | document | award
name: NASA APOD Gallery
summary: Self-updating gallery of NASA's Astronomy Picture of the Day.
sensitivity: public # public | private-repo | private
url: https://unduslabs.com/projects/nasa-gallery/
provenance:
- url:https://unduslabs.com/projects/nasa-gallery/
facts:
- text: Runs for under $0.01/month.
provenance: url:https://unduslabs.com/writing/nasa-apod-serverless-gallery/
edges:
- { rel: built-at, to: employer-independent }
- { rel: uses, to: tech-aws-lambda }
- { rel: published-as, to: article-nasa-apod-serverless-gallery }
A small, reused edge vocabulary keeps traversals predictable: worked-at, built-at, uses, published-as, about, measured-by, evidenced-by, cites. Facts can override the entity's sensitivity individually, so a public project can carry private notes.
The Server
The architecture is deliberately boring. Files hold the truth; a small script indexes them in memory; MCP exposes the queries; every assistant you use connects to the same thing:
flowchart LR
Y["YAML files
facts · sources · sensitivity"] --> S["server.py
networkx in memory"]
S --> T["MCP tools
whoami · find · entity · path"]
T --> C1["Claude Code"]
T --> C2["Claude Desktop"]
T --> C3["Cursor · any MCP client"]
The whole thing. Save as graph/server.py, with your YAML in graph/entities/:
"""Personal knowledge graph MCP server. Files are the source of truth;
this process is just an ephemeral index."""
import os
from pathlib import Path
import networkx as nx
import yaml
from mcp.server.fastmcp import FastMCP
ROOT = Path(__file__).parent
RANK = {"public": 0, "private-repo": 1, "private": 2}
MAX_RANK = {"public": 0, "private": 2}.get(os.environ.get("GRAPH_AUDIENCE", "public"), 0)
def visible(sensitivity):
if not sensitivity:
return True
return RANK.get(sensitivity, 2) <= MAX_RANK # unknown values fail safe to private
def load_graph():
g = nx.MultiDiGraph()
docs = []
for f in sorted((ROOT / "entities").rglob("*.yaml")):
docs += [d for d in yaml.safe_load_all(open(f)) if d]
edges = []
for doc in docs:
if not visible(doc.get("sensitivity")):
continue
g.add_node(doc["id"],
type=doc.get("type"), name=doc.get("name", doc["id"]),
summary=doc.get("summary", ""), url=doc.get("url"),
provenance=doc.get("provenance", []),
facts=[x for x in doc.get("facts", []) if visible(x.get("sensitivity"))])
edges += [(doc["id"], e) for e in doc.get("edges", [])]
for src, e in edges:
if g.has_node(e.get("to")):
g.add_edge(src, e["to"], **{k: v for k, v in e.items() if k != "to"})
return g
G = load_graph()
mcp = FastMCP("my-graph")
def brief(nid):
n = G.nodes[nid]
return {"id": nid, "type": n["type"], "name": n["name"], "summary": n["summary"]}
@mcp.tool()
def whoami() -> dict:
"""The person this graph is about, plus what the graph contains."""
people = [n for n, d in G.nodes(data=True) if d["type"] == "person"]
return {"person": entity(people[0]) if people else None,
"entities": G.number_of_nodes(), "edges": G.number_of_edges()}
@mcp.tool()
def find(query: str) -> dict:
"""Search entities by substring across names, summaries, and facts.
count 0 means no match, not an error."""
q = query.lower()
hits = [brief(n) for n, d in G.nodes(data=True)
if q in " ".join([d["name"], d["summary"]] +
[f.get("text", "") for f in d["facts"]]).lower()]
return {"query": query, "count": len(hits), "results": hits[:10]}
@mcp.tool()
def entity(id: str) -> dict:
"""One entity in full: facts with provenance, edges in both directions."""
n = G.nodes[id]
return {**brief(id), "url": n["url"], "provenance": n["provenance"], "facts": n["facts"],
"edges_out": [{**d, "to": v} for _, v, d in G.out_edges(id, data=True)],
"edges_in": [{**d, "from": u} for u, _, d in G.in_edges(id, data=True)]}
@mcp.tool()
def path(from_id: str, to_id: str) -> list:
"""How two entities connect: the shortest chain of relationships."""
nodes = nx.shortest_path(G.to_undirected(as_view=True), from_id, to_id)
return [{"from": G.nodes[a]["name"],
"rel": "/".join(sorted({d.get("rel", "?") for d in
(G.get_edge_data(a, b) or G.get_edge_data(b, a)).values()})),
"to": G.nodes[b]["name"]} for a, b in zip(nodes, nodes[1:])]
mcp.run()
That is the entire engine. I tested this exact listing against my real graph over the MCP wire protocol before publishing it.
Plug It In
pip install mcp networkx pyyaml
claude mcp add my-graph -- python3 /absolute/path/to/graph/server.py
That registers it with Claude Code. For any other MCP client (Claude Desktop, Cursor, and the rest), the standard config block is:
{
"mcpServers": {
"my-graph": {
"command": "python3",
"args": ["/absolute/path/to/graph/server.py"]
}
}
}
Open a new session and ask "what do you know about me?". The assistant discovers whoami, find, entity, and path on its own and starts querying.
If those commands read as routine to you, that really is the whole setup. If not, expand this:
Step-by-step setup, assuming nothing
What you need: Python 3.10 or newer, and at least one AI app that speaks MCP (Claude Code, Claude Desktop, or Cursor).
-
Check Python is installed. Open a terminal (on Windows: PowerShell) and run
python3 --version(Windows:python --version). If you get a version number, you are set. If not, install it from python.org/downloads. -
Install the three libraries. The most reliable form on any system:
python3 -m pip install mcp networkx pyyaml
(Windows: python -m pip install mcp networkx pyyaml.)
-
Create the folder. Anywhere you like, make a folder
graph/containingserver.py(the script above) and a subfolderentities/with your YAML files. -
Find the absolute path. "Absolute" means the full path from the root of your disk, like
/Users/you/graph/server.pyorC:\Users\you\graph\server.py. From inside thegraphfolder,pwd(macOS/Linux) orcdon a line by itself (Windows) prints it. -
Register the server in your AI app:
- Claude Code:
claude mcp add my-graph -- python3 /Users/you/graph/server.py - Claude Desktop: Settings, then Developer, then Edit Config. That opens
claude_desktop_config.json; add themcpServersJSON block above at the top level, save, then fully quit the app (from the system tray on Windows, not just the window) and relaunch. - Cursor: Settings, then MCP, then Add Server, same JSON shape.
- Windows + WSL note: if your Python and files live inside WSL but Claude Desktop runs on Windows, bridge the command through WSL:
"command": "wsl","args": ["-e", "python3", "/home/you/graph/server.py"].
Honestly, the easiest route: paste the JSON block into your AI and say "add this MCP server to my Claude Desktop config, adapting the command and path to my system." It knows where the config file lives and will handle the details.
- Claude Code:
-
Verify. Open a brand-new conversation (MCP servers load at session start, so it must be new) and ask "what do you know about me?".
If something fails: command not found: python3 means Python is not installed (step 1). No module named mcp means pip installed into a different Python; rerun step 2 exactly as written. Tools not appearing usually means the session predates the registration (open a new one) or the path is not absolute.
Or skip all of this: the prompt in the next section makes your assistant do the setup for you.
Don't Write the YAML by Hand
The best part: you already have an assistant that can build the graph for you. Paste something like this into your AI tool of choice:
Create a personal knowledge graph I can serve over MCP.
1. Make a folder graph/ with entities/ inside.
2. Use this schema for every entity: [paste the schema section above]
3. Read my resume (attached) and draft my entities: one person,
my employers, my main projects, the technologies I use, and any
articles or talks. One YAML document per entity, one file per type.
4. Every fact needs a provenance line. Mark anything that is not
already public as sensitivity: private-repo.
5. Save the server script from the article as graph/server.py,
install mcp, networkx, and pyyaml, and register the server.
6. Then interview me for ten minutes about what is missing.
Step 6 is where it gets good. The assistant asks about gaps ("this job has no projects, what did you actually build there?"), and the answers become sourced facts instead of vanishing when the session ends.
What It Looks Like in Use
Here is the experience, a real conversation against my graph (the same public tier that mirrors this site):
What do a NASA photo gallery and a Chilean payments company have in common?
In Sebastian's graph, five hops that read like his story: the NASA gallery is a Python side project, like Bills (his cash-flow SaaS); both are chapters of a 15-year builder arc, and the subject of that arc is Sebastian, who was Transformation Manager at Transbank, Chile's dominant card-payment processor, from 2016 to 2018.
And what do you know about that NASA gallery?
An independent project: a self-updating gallery of NASA's Astronomy Picture of the Day, built on AWS Lambda and S3, running for under $0.01 a month (source: the tutorial on unduslabs.com), and published as its own how-to article.
No pasted bio, no lucky guessing: the assistant answered by querying. Under the hood it composed tool calls against the graph.
The first answer is one path call, verbatim:
[
{ "from": "NASA APOD Gallery", "rel": "uses",
"to": "Python" },
{ "from": "Python", "rel": "uses",
"to": "Bills, a Cash-Flow Projection App" },
{ "from": "Bills, a Cash-Flow Projection App", "rel": "part-of",
"to": "The builder arc, 15 years from lean to AI" },
{ "from": "The builder arc, 15 years from lean to AI", "rel": "about",
"to": "Sebastian Undurraga" },
{ "from": "Sebastian Undurraga", "rel": "worked-at",
"to": "Transbank" }
]
Look at the middle stop: a story entity. The answer reads like a narrative because the narrative is stored in the graph, not improvised by the model.
The second is one entity call (trimmed here), and note that every fact arrives with its source attached:
{
"id": "project-nasa-apod-gallery",
"name": "NASA APOD Gallery",
"facts": [
{ "text": "Runs for under $0.01/month.",
"provenance": "url:https://unduslabs.com/writing/nasa-apod-serverless-gallery/" }
],
"edges_out": [
{ "rel": "built-at", "to": "employer-independent" },
{ "rel": "uses", "to": "tech-aws-lambda" },
{ "rel": "uses", "to": "tech-amazon-s3" },
{ "rel": "published-as", "to": "article-nasa-apod-serverless-gallery" }
]
}
The answer to "what should I say about this project in a cover letter?" is now grounded in facts with receipts, not the model's best guess about what sounds impressive.
One Graph, Every Tool
This is the quiet superpower. MCP is an open standard, so the same little server plugs into every assistant you use: Claude Code in the terminal, Claude Desktop, Cursor, whatever comes next. You describe yourself once, and every tool knows you.
And because the data is YAML in a folder you own, there is no lock-in on the other side either. Switch AI vendors, switch graph engines, or just read your own files: the graph outlives every tool choice around it.
What It's Good For
- Never introduce yourself again. Any session, any tool, any machine with the config: "what do you know about me?" just works.
- Write about yourself with receipts. Resume tailoring, cover letters, bios, performance-review packets: every claim traceable to a source, which matters when accuracy matters.
- Interview prep. "Which of my projects best answers a question about legacy migrations?" is a
findplus a couple ofentitycalls. - Onboard a new tool in seconds. New editor, new agent, new laptop: one config block and it has your full context.
- A privacy boundary that is structural. The private tier is not a promise to be careful; it is a filter the public server physically cannot cross.
- A hub every connected tool can feed. If your assistant is connected to your email, calendar, cloud docs, or repos, each connection is a source. The contract that arrived by email becomes a fact with provenance; the decision buried in a shared doc becomes an edge between a project and a document; the side project in a repo becomes an entity with its stack. The graph is where context scattered across tools consolidates into something queryable.
- It grows as you work. The files are plain YAML, so the same assistant that queries the graph can extend it. Say "remember this" when something matters, or make it automatic with a standing instruction in your assistant's config: "when you learn a durable fact about me, add it to my graph with a source." Review the diff before you commit, and the record stays trustworthy.
This last part is what compounds. A bio is a snapshot that goes stale the day you write it. A graph wired into the tools you already use accretes instead: every email thread, document, and repository your assistant touches can leave behind a sourced fact, and a year from now "what do you know about me?" is not the same paragraph you pasted everywhere. It is a living record, with receipts.
Where This Goes
My own version has grown past the listing above: write tools let any of my sessions save new facts (staged in an inbox file for review, never published automatically), so the graph accretes as I work. Next steps: a hosted read-only endpoint so any AI, not just mine, can ask who I am; syncing the graph across machines; and open-sourcing the full version.
The context problem is the defining constraint of working with AI right now. For organizations, I believe the answer is a metadata-first knowledge graph. It turns out the same is true for a person. Remember yourself.