Install
$ agentstack add mcp-alexbojko-hopai ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
🐘 hopai
A knowledge graph in the Postgres you already run — no graph database required.
[](https://github.com/alexbojko/hopai/actions/workflows/ci.yml)
[](https://github.com/astral-sh/ruff) [](LICENSE)
hopai compiles multi-hop graph traversals into a single recursive CTE against two ordinary PostgreSQL tables (nodes, edges, each with a JSONB properties bag) — no extension, no sidecar service. Traversal, ingestion, updates, real constraints and vector search, through a Python API, a JSON one, and a Cypher subset, so an LLM agent and a human developer can both use it with nothing new to learn.
> Early and moving. hopai is pre-1.0 (0.0.x). Every feat/fix > release may change an interface — a method signature, a JSON key, a > refusal's exact wording — without a deprecation cycle; pin a version if > that would break you. Schema migrations don't exist yet either: > drop_schema() + create_schema() is the upgrade path for now. See the > changelog > before bumping.
⚡ Quick start
pip install hopai
from sqlalchemy import create_engine
from hopai import Graph, Start, Hop
graph = Graph(create_engine("postgresql+psycopg2://user:pass@host/db"))
# "Which companies do Alice's friends work for — counting friends of
# friends, up to four hops out, and only people who are still active?"
result = graph.traverse(
Start(where={"name": "Alice"}),
Hop(via={"kind": "friend"}, hops=(1, 4), where={"active": True}),
Hop(via={"kind": "works_at"}, where={"type": "company"}),
)
result.nodes # [{"id": "1", "properties": {...}}, ...]
result.edges # [{"id": "7", "start_id": "1", "end_id": "2", "properties": {...}}, ...]
result.to_networkx() # in-memory graph, if you have networkx installed
Read a traversal left to right as a sentence: start here → follow these edges this many times → land on nodes like this → then again. You get back the whole matching subgraph, not just the endpoints. The full walkthrough is [01_quickstart](notebooks/01_quickstart.ipynb).
🔭 Every way to ask
One engine underneath all of it — the same "Alice's friends" question as above, other front ends that ask it, and the other things this engine answers:
# JSON -- for an LLM tool call, an HTTP handler, or config-driven traversal
traverse_json(graph, {"start": {"where": {"name": "Alice"}},
"hops": [{"via": {"kind": "friend"}, "hops": [1, 4]}]})
# Cypher -- for a caller, or a model, that already thinks in it
graph.cypher("MATCH (a:person {name: 'Alice'})-[:friend*1..4]->(b) RETURN b")
# Aggregate -- a number instead of a subgraph, computed in the database
graph.aggregate(Start(where={"name": "Alice"}),
Hop(via={"kind": "friend"}, hops=(1, 4)),
aggregates={"count": Count()}) # {"count": 12}
# Change and delete -- the same filters, selecting rows to update or remove
graph.update_nodes(where={"name": "Alice"}, set={"active": False})
Similarity and traversal compose — find the nodes closest in meaning to some text, then walk the graph from there. No separate vector database, no gluing two systems together with application code:
# Vector search on its own -- exact cosine similarity, no pgvector, no extension
graph.vector_search(Near("summary", "distributed consensus"), k=10,
where={"type": "paper"})
# Start a traversal from similarity instead of a property match: the 5 papers
# most similar to the text, then everything they cite, up to 3 hops out.
graph.traverse(
Start(near=Near("summary", "distributed consensus"), keep=5),
Hop(via={"kind": "cites"}, hops=(1, 3)),
)
Hop(near=, keep=) and Hop(via_near=, via_keep=) do the same thing mid-chain — rank what a hop reaches, or beam over each node's edges, by similarity instead of only filtering by property. See Vector search in the table below.
Reranking is the third stage — retrieve wide and cheap with the cosine above, then actually read each candidate against the query before keeping k. It attaches to a flat search and to a traversal step alike:
import cohere
from hopai import Rerank
reader = Rerank(cohere.ClientV2(), model="rerank-v3.5",
document_from='.properties.title + ": " + .properties.summary',
candidates=50) # pool the reranker sees
graph.vector_search(Near("summary", "distributed consensus"), rerank=reader, k=10)
No provider package imported here either — a Cohere/Voyage client, a sentence-transformers CrossEncoder, or a plain callable all work. See Reranking in the table below.
Every one of these compiles through the same query builder, so the SQL, the semantics and the invariants are identical no matter which front end wrote the call. The ["Learn more"](#-learn-more) table below is where each one's full reference lives.
✨ Highlights
- 🐘 Plain PostgreSQL — two tables, a recursive CTE, no extension, no
new operational dependency. (One opt-in exception, off by default: vector_backend="pgvector".)
- 🧭 Real multi-hop traversal — bounded and unbounded hops, per-hop
direction, OPTIONAL, rich JSONB filtering, one round trip.
- ✏️ Update and delete by filter —
SET/REMOVE/DETACH DELETE
semantics, with a filterless call refusing rather than emptying the graph.
- 🧮 In-database aggregation —
count/sum/avg/min/max
computed where the data lives.
- 🧬 Many graphs, one database — a graph is a string, not a schema;
cross-graph edges are impossible by construction (composite FK).
- 🤖 Three front ends, one engine — Python, JSON (with a ready-made LLM
tool schema), and a Cypher subset all compile through the same builder.
- 🔌 An MCP server in one command —
hopai-mcpexposes reading, writing,
schema and similarity tools, with permissions deciding which tools exist.
- 🌐 An HTTP API and a graph explorer —
hopai-apiserves the same
operations as JSON for a browser, and a self-contained page at / that draws the graph, filters it, and edits it under the same permissions.
- 🔐 Constraints Neo4j puts behind an enterprise licence — unique,
composite, partial, existence, type and CHECK constraints on JSONB.
- 🧲 Similarity-seeded traversal — find the nodes closest in meaning to
some text, then walk the graph from there, in one call. Exact cosine similarity on plain real[] columns by default — no pgvector, multivector queries, and a field-level embed= so you can hand it text instead of floats — plus an opt-in vector_backend="pgvector" for when the row counts outgrow exact search.
- 🎯 Reranking, including inside the walk — `Rerank(client,
document_from='') adds a third retrieval stage to a search *and* to a traversal step, where a candidate is a node plus how it was reached. A model may write the projection: it's validated against a total jq subset in which env` doesn't parse.
- 🧪 Tested like it matters — an 85% coverage gate and mutation testing
in CI, and real benchmark numbers in benchmarks/.
🗄️ Schema
graph.create_schema() # idempotent; safe to call on every start-up
CREATE TABLE nodes (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
properties JSONB NOT NULL DEFAULT '{}'
);
CREATE TABLE edges (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
start_id BIGINT NOT NULL REFERENCES nodes(id),
end_id BIGINT NOT NULL REFERENCES nodes(id),
properties JSONB NOT NULL DEFAULT '{}'
);
CREATE INDEX ON edges (start_id);
CREATE INDEX ON edges (end_id);
CREATE INDEX ON nodes USING GIN (properties);
CREATE INDEX ON edges USING GIN (properties);
Custom table/column names, and extra real columns alongside the JSONB bag (a foreign key to your own users table, with the collision refusals that keep it distinct from a JSONB property) are covered in full in Schema; multi-graph isolation on one connection pool in [07_many_graphs](notebooks/07manygraphs.ipynb) and Many graphs.
📚 Learn more
Nothing below is summarized away — every section the README used to spell out inline now has a full write-up in one of three places, and each links to the others: a runnable notebook for the topics that have one (executed in CI on every PR, so it can't drift from the API), a guide under Reference explaining the semantics and the gotchas a notebook doesn't narrate, and a generated API reference — hopai.core for Graph itself, and one page per module for everything else — built straight from the library's own docstrings and signatures, so it is the one tier of this table that is structurally unable to go stale:
| Topic | Notebook | Full reference | | --- | --- | --- | | Schema — two tables, extending the model with real columns | [08_under_the_hood](notebooks/08underthehood.ipynb) | Schema | | Many graphs, one database | [07_many_graphs](notebooks/07manygraphs.ipynb) | Many graphs | | Getting data in — add_nodes/add_edges/merge_*/ingest | — | Getting data in | | Changing and deleting — update_*/delete_*/clear/mutate | — | Changing and deleting | | Constraints — unique, composite, partial, existence, type, CHECK | [05_constraints](notebooks/05constraints.ipynb) | Constraints | | Declaring, inferring and enforcing a graph schema | [06_graph_schema](notebooks/06graphschema.ipynb) | Graph schema | | Filters — AND/OR/NOT/GT/BETWEEN, the escape hatch | — | Filters | | Traversal: direction, hop count, OPTIONAL | [02_traversal](notebooks/02traversal.ipynb) | Traversal | | Aggregation | [03_aggregation](notebooks/03aggregation.ipynb) | Aggregation | | Vector search, hybrid ranking, text-to-vector embedding | [09_vector_search](notebooks/09vectorsearch.ipynb) | Vector search | | Reranking — the third retrieval stage, including step-wise | [10_reranking](notebooks/10reranking.ipynb) | Reranking | | The JSON interface | [04_json_and_cypher](notebooks/04jsonandcypher.ipynb) | JSON interface | | Cypher as input syntax | [04_json_and_cypher](notebooks/04jsonandcypher.ipynb) | Cypher | | What this doesn't do (yet), and why each refusal is a refusal | — | Limits | | MCP server — client setup, every tool, every flag | — | Full guide | | HTTP API and the graph explorer | — | Full guide | | Read/write pipelines, multi-graph internals, gotchas | — | architecture.md | | Fixtures, coverage gate, mutation testing | — | testing.md | | release-please, PyPI trusted publishing | — | releasing.md |
See [notebooks/README.md](notebooks/README.md) for how to run the notebooks yourself against a throwaway database.
🔌 MCP server
The same graph as an MCP server, so Claude Desktop, Claude Code, an IDE or an agent framework can use it with nothing to write:
pip install "hopai[mcp]"
hopai-mcp --dsn postgresql+psycopg2://user:pass@localhost/db --read-only
Eleven tools — traverse, aggregate, Cypher, ingest, update/delete, schema inference/declaration, and similarity search. Permissions decide which tools exist: --read-only registers reading only, the default adds writing, --allow-mutations adds deleting, --allow-ddl adds enforce_schema. 📖 Full guide.
🌐 HTTP API and graph explorer
A browser cannot reasonably speak MCP — that is JSON-RPC with session negotiation over a streaming transport — so the same operations are served as ordinary JSON, plus a graph explorer at /:
pip install "hopai[http]"
hopai-api --dsn postgresql+psycopg2://user:pass@localhost/db --allow-mutations
# → explorer at http://127.0.0.1:8080/
Switch graphs, toggle node types, filter by name or by property values, drag and pin nodes, show edge labels, repoint an edge, delete a row. The page reads the permissions the server was started with and hides what it would refuse; every delete takes two clicks and names what will go. The whole page is one file with no CDN, shipped in the wheel. 📖 Full guide.
docker compose up -d brings up Postgres, this API and the MCP server together.
⏱️ Async
AsyncGraph (pip install hopai[asyncio]) covers traversal, aggregation, ingestion, mutation and vector search for an async app — the same query builders Graph runs, reached through SQLAlchemy's sync/async bridge. Schema and constraint declaration stay on the sync Graph — one-time setup calls with no concurrency to gain. See hopai/asyncio.py and the Async section of architecture.md for the bridge design and the benchmark behind it.
🚧 What this doesn't do (yet)
- No disjoint multi-pattern matching — one linear chain of hops only.
OPTIONALonly on the last hop, not mid-chain.- Aggregation covers
count/sum/avg/min/maxover the last step's
matched nodes, numeric properties only — no grouping, no stddev/percentiles.
- Deletes and updates select rows by their properties, never by where a
traversal arrived.
- Vector search is exact and unindexed by design — no ANN, no
late-interaction multivectors.
- Embedding retries transient failures but does not cache or rate-limit —
that's the application's and the client's job, respectively.
- A cycle-protection path array on every recursive row is measurably
not-cheap past roughly 10 hops on a single-segment traversal.
Each refusal names the rewrite rather than approximating — see the full list for the reasoning behind each one, and architecture.md / hopai/vectors.py/hopai/cypher.py for the implementation.
🛠️ Development
pip install -e ".[dev]"
docker compose up -d # throwaway PostgreSQL matching the default DSN
pytest tests/ -v
ruff check .
Most of the suite needs no database — query shape, filter compilation and the Cypher translator are tested against compiled SQL. CI enforces an 85% line coverage floor and runs mutation testing on every PR. See testing.md.
📄 License
MIT — see [LICENSE](LICENSE).
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: alexbojko
- Source: alexbojko/hopai
- License: MIT
- Homepage: https://hopai.readthedocs.io/
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.