Use the best AI for every task, not the same AI for every task.
oh-my-graph gives AI agents a shared, persistent knowledge graph β implemented as an MCP server β so different assistants can collaborate through the same continuously updated context instead of starting from zero every session. See Why oh-my-graph for the full idea, or a concrete example of it in action.
macOS β installs as a launchd service that starts automatically on login:
brew install h0n9/devops/oh-my-graph
brew services start h0n9/devops/oh-my-graph
If you'd like to share the graph across multiple Macs, see Syncing across devices.
Linux (or macOS without Homebrew) β one-line installer, detects OS/arch and installs to ~/.local/bin (override with INSTALL_DIR; pin a version with VERSION=vX.Y.Z):
curl -fsSL https://raw.githubusercontent.com/h0n9/oh-my-graph/main/install.sh | sh
The server runs on port 7780 by default.
Prefer not to run it on your own machine? Host oh-my-graph as an always-on remote MCP server on any platform that can run the Go binary and expose a port β a VPS, a cloud VM, a container host, etc. The walkthrough below uses Sprites as one concrete example; the same binary and flags work anywhere.
Example: deploying on Sprites
curl -fsSL https://sprites.dev/install.sh | sh
sprite org auth
sprite create oh-my-graph
sprite use oh-my-graph
Install the release binary onto the sprite with the same installer used locally:
sprite exec -- bash -c "export INSTALL_DIR=/home/sprite; curl -fsSL https://raw.githubusercontent.com/h0n9/oh-my-graph/main/install.sh | sh"
Register it as a persistent service, bound to the sprite's public port so it restarts across hibernation/reboot:
sprite exec -- sprite-env services create oh-my-graph --cmd /home/sprite/oh-my-graph --args "--port,7780,--data,/home/sprite/.oh-my-graph" --http-port 7780
Migrating existing data: tar up your local data directory and upload it before starting the service:
tar -czf data.tar.gz -C ~/.oh-my-graph .
sprite exec --file "data.tar.gz:/home/sprite/data.tar.gz" -- bash -c "mkdir -p /home/sprite/.oh-my-graph && tar -xzf /home/sprite/data.tar.gz -C /home/sprite/.oh-my-graph && rm /home/sprite/data.tar.gz"
Whether authentication is required depends on network exposure, not on where you host oh-my-graph. If the server is only ever reached via localhost, no auth is needed β that's the default for a local install. The moment the port is bound to a non-localhost interface, or exposed through a reverse proxy, tunnel, or port forward β self-hosted on a VPS/cloud box, or hosted on Sprites β you should enable auth.
Enable it with --auth, which requires two environment variables:
OMG_ISSUER=https://your-public-base-url
OMG_OWNER_PASSPHRASE=<a-secret-only-you-know>
This turns on a full OAuth 2.1 Authorization Code + PKCE flow with Dynamic Client Registration β MCP clients self-register and your browser prompts once for the passphrase; bearer tokens on /mcp and /omg-mcp authorize every call after that. The web visualization UI (/ and /graph) is protected separately, gated by the same passphrase via an in-app /login page (a session cookie, not a browser Basic Auth prompt).
Recommendation: treat any non-localhost binding as a public endpoint by default and require --auth, paired with standard network hygiene β HTTPS termination via reverse proxy, firewall rules limiting source IPs.
See Connecting AI Clients β Connecting to a remote server for a worked walkthrough of this mechanism, plus a platform-specific shortcut for clients that don't need it.
Point your MCP client at http://localhost:7780/mcp (Streamable HTTP transport, JSON-RPC 2.0) for a local install, or at your remote server's public URL for a remote one.
Claude Desktop only supports stdio-based MCP servers. Use mcp-remote as a bridge to the HTTP server.
Add the following to your Claude Desktop config file (macOS: ~/Library/Application Support/Claude/claude_desktop_config.json; Windows: %APPDATA%\Claude\claude_desktop_config.json):
{
"mcpServers": {
"oh-my-graph": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"http://localhost:7780/mcp"
]
}
}
}
Then restart Claude Desktop. The oh-my-graph tools (list_topics, get_topic, read_nodes_since_cursor, read_nodes_since_date, read_hub_nodes, read_node, neighbors, write) will appear automatically.
Claude Code natively supports Streamable HTTP MCP β no bridge required.
Via CLI (global scope, so the server is available from every project):
claude mcp add oh-my-graph --transport http --scope user http://localhost:7780/mcp
Manually β add to ~/.claude.json (global) or .claude/settings.json (project):
{
"mcpServers": {
"oh-my-graph": {
"type": "http",
"url": "http://localhost:7780/mcp"
}
}
}
Tip: Add the following to your ~/.claude/CLAUDE.md so Claude automatically loads graph context at the start of every session:
## oh-my-graph Knowledge Graph
At the start of every session, connect to the `oh-my-graph` MCP server:
1. Call `list_topics` to discover existing topics.
2. Infer the topic from context β working directory name, project name, or the user's first message.
3. With no specific retrieval intent, call `read_hub_nodes(<topic>)` for a compact map of the topic's most-connected nodes. Select relevant anchors from the result.
4. Call `neighbors(<topic>, node_id, depth: 2)` on selected anchors to pull in graph-local context, then use `read_node` only for the final nodes that need full details. Use `read_nodes_since_date` for time-scoped context or `read_nodes_since_cursor` when resuming from a known checkpoint.
During the session, call `write` frequently to persist findings, decisions, and artifacts. Link related nodes with edges to preserve reasoning chains.
Add to ~/.codex/config.yaml:
mcp_servers:
- name: oh-my-graph
type: http
url: http://localhost:7780/mcp
See Security and Authentication for when you need this. Option B is oh-my-graph's own platform-agnostic auth mechanism and works identically on any host; Option A is a shortcut specific to this platform's own gateway.
Option A β Sprite gateway auth (default on this platform). Leave the sprite's URL at its default sprite auth mode and run without --auth. Any client with a valid Sprites org bearer token can connect:
curl -H "Authorization: Bearer $SPRITE_TOKEN" https://<sprite>-<org>.sprites.app/omg-mcp
This doesn't work for clients with no header/static-credential field β notably ChatGPT. Use Option B for those.
Option B β OMG OAuth (opt-in). Run with --auth and set OMG_ISSUER (the server's public base URL) and OMG_OWNER_PASSPHRASE (a shared secret). This enables a full OAuth 2.1 Authorization Code + PKCE flow with Dynamic Client Registration, gated by the passphrase:
sprite exec -- sprite-env services create oh-my-graph --cmd /home/sprite/oh-my-graph --args "--port,7780,--data,/home/sprite/.oh-my-graph,--auth" --env "OMG_ISSUER=https://<sprite>-<org>.sprites.app,OMG_OWNER_PASSPHRASE=<your-passphrase>" --http-port 7780
sprite url update --auth public -s oh-my-graph
Then just add https://<sprite>-<org>.sprites.app/omg-mcp as the connector URL in Claude or ChatGPT β no header, no client ID or secret needed. The client self-registers via DCR and your browser prompts once for the passphrase.
Note: Sprites' own gateway reserves
/mcpon every*.sprites.appURL for its own control-plane server, sooh-my-graphalso serves its MCP handler at/omg-mcpβ use that path when connecting through a sprite's public URL.
Modern AI assistants are powerful, but every conversation is an island. The moment you switch from ChatGPT to Claude to any other assistant, the context you built disappears β ideas, decisions, and architecture choices have to be re-explained from scratch.
oh-my-graph solves this by separating knowledge from conversations, giving different assistants a shared, persistent layer to collaborate through instead of starting from zero every session.
Every new session β a new terminal, a new agent, a new person on the team β normally starts from zero, no matter how much a previous session figured out. Dropbox stores your files. Git stores the evolution of your project. Chat history stores a conversation. oh-my-graph stores the evolution of your project's understanding β the findings, decisions, and open questions multiple agents accumulate while working on it, so the next session picks up where the last one left off.
Concretely, that means:
- Persist findings across sessions
- Share knowledge between concurrent agents working on the same project
- Pass messages between sessions using
messagenodes andreplies_toedges - Track reasoning with
supports,contradicts,causes,deprecatesedges
Scenario: Turning a walking conversation into an implemented feature
A developer has an idea while walking outside.
Step 1 β Capture ideas naturally with ChatGPT Voice
While walking, the developer opens ChatGPT Voice and talks naturally: "I think AI assistants are becoming more powerful, but the biggest problem is that my knowledge is scattered across different conversations. I want a way to keep my ideas and context available everywhere."
Instead of writing notes manually, the developer explores the idea conversationally. ChatGPT helps by asking questions, discovering missing perspectives, structuring the idea, identifying use cases, and summarizing decisions. The important insights are stored in oh-my-graph:
- Problem: "AI conversations are isolated across platforms."
- Insight: "Knowledge should exist independently from any AI assistant."
- Decision: "
oh-my-graphshould become a shared context layer."
Step 2 β Continue implementation with Claude Code
Later, back at the desk, the developer opens Claude Code. It doesn't need to be told everything again β it reads the existing graph context: the original problem, previous discussions, architecture decisions, technical constraints, implementation ideas. It proposes a plan: "Based on the graph context, we should implement a new MCP integration that allows AI clients to access shared knowledge nodes." The developer reviews it, says "Yes, implement it," and Claude Code writes the code.
Step 3 β Use another AI for communication and marketing
After implementation, the developer brings in an assistant that's stronger at communication. It reads the same graph context and helps with README improvements, blog posts, product positioning, documentation, and user guides β understanding the original vision because the context was already preserved.
The result
The developer is no longer switching between disconnected AI conversations:
- ChatGPT Voice β explores ideas
oh-my-graphβ preserves knowledge- Claude Code β builds the solution
- Other AI assistants β communicate and expand the idea
Each AI does what it does best. The user's knowledge remains continuous β your AI assistants may change, but your knowledge stays with you.
oh-my-graph runs as an HTTP server exposing a Model Context Protocol (MCP) interface. Multiple AI agents connect to a single server instance and share knowledge organized into topics.
Knowledge is stored as a graph of nodes (facts, concepts, questions, decisions, messages, ...) and edges (causal, epistemic, conversational relationships). The graph is persisted as an append-only JSONL file (graph.jsonl) per topic β like a write-ahead log:
~/.oh-my-graph/
βββ life/
β βββ graph.jsonl
βββ project-x/
β βββ graph.jsonl
βββ comms/
βββ graph.jsonl
Open http://localhost:7780/ in your browser to explore the graph visually β the topic list shows node/edge counts, and /graph?topic=<name> renders a live interactive force-directed graph.
{
"node_id": "uuid-v4",
"type": "finding | concept | blocker | question | decision | artifact | entity | event | message",
"summary": "one-liner",
"description": "full markdown body",
"confidence": 0.92
}
confidence is optional. An explicit value of 0 is preserved; when omitted, it is absent from read responses.
| Type | Purpose |
|---|---|
finding |
A discovered fact or observation |
concept |
An abstract idea or principle |
blocker |
Something preventing progress |
question |
An open unknown |
decision |
A made choice with rationale |
artifact |
A produced item (file, PR, doc) |
entity |
A real-world thing (person, system, service) |
event |
Something that happened |
message |
An inter-session message (see Messaging) |
{
"edge_id": "uuid-v4",
"type": "resolves | produces | blocks | causes | supports | contradicts | depends_on | part_of | references | replies_to | deprecates",
"from_node_id": "uuid-v4",
"to_node_id": "uuid-v4"
}
| Type | Meaning |
|---|---|
resolves |
Solution β blocker |
produces |
Process β artifact |
blocks |
Blocker β target |
causes |
Cause β effect |
supports |
Evidence β claim |
contradicts |
Counter-evidence β claim |
depends_on |
A requires B |
part_of |
A belongs to B |
references |
A cites B |
replies_to |
Message β message (threading) |
deprecates |
New node supersedes old node |
Each line is a WAL record:
{"seq":1,"type":"node","ts":"2026-06-18T12:00:00Z","data":{"node_id":"550e8400-e29b-41d4-a716-446655440000","type":"finding","summary":"Redis cache hit rate dropped to 40% after v2.3 deploy","description":"After deploying v2.3, Redis cache hit rate fell from 85% to 40%. Root cause: key prefix change in the new config loader.","confidence":0.92}}
{"seq":2,"type":"edge","ts":"2026-06-18T12:00:01Z","data":{"edge_id":"660e8400-e29b-41d4-a716-446655440001","type":"causes","from_node_id":"550e8400-e29b-41d4-a716-446655440000","to_node_id":"770e8400-e29b-41d4-a716-446655440002"}}
seqβ monotonically increasing sequence number (the cursor)tsβ wall-clock time of append (RFC 3339)- Records are never modified or deleted β use a
deprecatesedge instead
| Tool | Signature | Returns |
|---|---|---|
list_topics |
() |
[]string |
get_topic |
(topic) |
{last_cursor, node_count, edge_count} |
read_nodes_since_cursor |
(topic, cursor?, types?, sort?) |
[]{node_id, type, summary, seq} |
read_nodes_since_date |
(topic, date, types?, sort?) |
[]{node_id, type, summary, seq} |
read_hub_nodes |
(topic, limit?, types?) |
[]{node_id, type, summary, seq, neighbor_count, confidence?} |
read_node |
(topic, node_id) |
full node + all edges (in & out) |
neighbors |
(topic, node_id, depth?, direction?, edge_types?, limit?) |
{anchor, neighbors: []{node_id, type, summary, seq, hop, via_edge}, truncated} |
write |
(topic, nodes[], edges[]) |
{cursor} |
cursor defaults to 0; pass -1 to return the newest nodes first (respecting limit, regardless of sort). date accepts an RFC3339 timestamp or a plain YYYY-MM-DD. sort defaults to "ascending"; pass "descending" for newest-first with an ordinary cursor. types defaults to ["finding"] when omitted; pass types:["*"] for every node type, or a specific list to narrow further.
neighbors does a BFS traversal from node_id out to depth hops (1β3, default 1), following direction (default both) and filtering by edge_types (default ["*"]). Returns summary-level neighbors capped at limit (default 50, max 200); truncated is true if the reachable set exceeded limit.
read_hub_nodes returns the highest-connectivity nodes for query-independent cold starts (limit default 10, max 50; types default ["*"]). Connectivity is the number of distinct neighbors after treating edges as undirected, collapsing parallel/opposite edges, and ignoring self-loops. types filters result candidates only; neighbors of every type still contribute to connectivity. Zero-neighbor nodes are omitted, so an edgeless topic returns []. Results are ordered by neighbor_count descending, then seq descending, then node_id ascending. Optional confidence is preserved as output metadata but never affects ranking.
Agents communicate asynchronously via message nodes in a shared topic:
- Session A writes a
messagenode to topic"comms" - Session B polls
read_nodes_since_cursor("comms", cursor: last_cursor, types:["message"])and sees the message - Session B replies with a new
messagenode +replies_toedge pointing back
No extra infrastructure needed β the graph is the message bus.
Start the server:
oh-my-graph # listens on :7780, data at ~/.oh-my-graph
oh-my-graph --port 8080 # custom port
oh-my-graph --data /var/omg # custom data directory
The server loads each topic graph into memory on first access and flushes writes to disk asynchronously. Multiple agents may connect concurrently.
Symlinking the data directory into iCloud Drive lets you share your graph across multiple Macs and browse it on iPhone.
Fresh install (no existing data):
mkdir -p "$HOME/Library/Mobile Documents/com~apple~CloudDocs/oh-my-graph"
ln -s "$HOME/Library/Mobile Documents/com~apple~CloudDocs/oh-my-graph" ~/.oh-my-graph
brew services start h0n9/devops/oh-my-graph
Existing data at ~/.oh-my-graph β back up first (cp -r ~/.oh-my-graph ~/.oh-my-graph.bak), then:
brew services stop h0n9/devops/oh-my-graph
mv ~/.oh-my-graph "$HOME/Library/Mobile Documents/com~apple~CloudDocs/oh-my-graph"
ln -s "$HOME/Library/Mobile Documents/com~apple~CloudDocs/oh-my-graph" ~/.oh-my-graph
brew services start h0n9/devops/oh-my-graph
On each additional Mac:
brew services stop h0n9/devops/oh-my-graph
rm -rf ~/.oh-my-graph
ln -s "$HOME/Library/Mobile Documents/com~apple~CloudDocs/oh-my-graph" ~/.oh-my-graph
brew services start h0n9/devops/oh-my-graph
Make sure only one machine runs the server at a time to avoid concurrent writes to the same file.
git clone https://github.com/h0n9/oh-my-graph
cd oh-my-graph
make run # go run β starts the server on port 7780
make build # produces ./oh-my-graph binary
make clean # removes the binary
Requires Go 1.26+. No external dependencies.
Measured on Apple M1 Pro (go test ./internal/graph/... ./internal/mcp/... -bench=. -run=^$ -benchmem).
| Benchmark | Scenario | Time/op | Memory/op | Allocs/op |
|---|---|---|---|---|
BenchmarkNodesSinceRareTypeFilter |
read_nodes_since_cursor filtered to a single type, with 1 matching node buried behind 50,000 nodes of another type |
57.5 ns | 64 B | 1 |
BenchmarkNodesSinceWildcard |
read_nodes_since_cursor with no type filter, over 50,000 nodes |
348 ns | 581 B | 1 |
BenchmarkNodesSinceMultiType |
read_nodes_since_cursor merging 3 types, 10,000 nodes each |
7.48 Β΅s | 16.0 KB | 9 |
BenchmarkGetNode |
read_node on a node with 2 edges, in a 10,000-node graph |
112 ns | 48 B | 2 |
BenchmarkNeighborsChain |
neighbors β depth 2, both directions, mid-chain anchor in a 10,000-node chain |
1.68 Β΅s | 6.8 KB | 11 |
BenchmarkNeighborsHub |
neighbors β depth 1 from a hub node with 10,000 outgoing edges, limit 50 |
10.7 Β΅s | 24.9 KB | 23 |
BenchmarkHubNodesSparse |
read_hub_nodes β top 10 from a 10,000-node, 9,999-edge chain |
2.76 ms | 3.68 MB | 257 |
BenchmarkWriteBatch |
write β single-node batch |
7.34 Β΅s | 1.6 KB | 11 |
BenchmarkWriteBatchLarge |
write β 50-node batch |
83.9 Β΅s | 68.2 KB | 362 |
BenchmarkWriteParallel |
write β single-node batches from concurrent callers |
7.48 Β΅s | 1.2 KB | 10 |
BenchmarkSnapshot |
full graph snapshot (backs /api/graph), 10,000 nodes + 5,000 edges |
138 Β΅s | 121 KB | 3 |
BenchmarkTopicLoad |
cold start: opening a topic backed by an existing 20,000-node WAL file | 55.9 ms | 24.0 MB | 320,230 |
| Benchmark | Scenario | Time/op | Memory/op | Allocs/op |
|---|---|---|---|---|
BenchmarkWriteHandler |
write tool call β JSON args in, Write, JSON result out |
7.91 Β΅s | 2.4 KB | 31 |
BenchmarkReadNodesSinceCursorHandler |
read_nodes_since_cursor tool call, default filter, over 1,000 seeded nodes |
17.7 Β΅s | 18.3 KB | 11 |
BenchmarkReadHubNodesHandler |
read_hub_nodes tool call, top 10 from a 1,000-node, 999-edge chain |
268 Β΅s | 437 KB | 46 |
BenchmarkNeighborsHandler |
neighbors tool call β depth 2, limit 50, mid-chain anchor in a 1,000-node chain |
4.30 Β΅s | 8.5 KB | 22 |
Reproduce locally:
go test ./internal/graph/... ./internal/mcp/... -bench=. -run=^$ -benchmem
Apache 2.0 β see LICENSE for details.
