An MCP server that exposes Lettria Perseus, Lettria's Text-to-Graph engine, to MCP-compatible clients (Claude Code, Claude Desktop, Cursor, Zed, etc.).
It wraps the official perseus-client Python SDK, so anything the SDK can do — ingest files, run ontology-driven extraction, build knowledge graphs, interlink them, and export to Neo4j / FalkorDB / TTL / Cypher — becomes a callable tool for an LLM agent.
Status: experimental scaffold. The upstream SDK is a
1.0.0rcpre-release; APIs may shift.
- Features
- Requirements
- Installation
- Configuration
- Running
- Tool reference
- Typical workflows
- Inline text vs. file paths
- Design notes
- Security
- Development
- License
- Related links
- 29 MCP tools covering the full Perseus workflow:
- Graph building (
build_graph) and merging (interlink_graphs) - An in-memory registry so graphs can be referenced across tool calls
- Paginated inspection of entities and relations
- Exports to TTL, Cypher (CQL) files, Neo4j, and FalkorDB
- Remote file, ontology, and job management
- Inline-text variants (
build_graph_from_text,upload_ontology_from_text) for agents without filesystem access
- Graph building (
- Async throughout — uses the SDK's
*_asyncvariants, so long extraction jobs don't block the event loop. - Safe by default —
PERSEUS_API_KEYis required at startup; the server refuses to run without it. - Pagination for large graphs — entities and relations are returned in slices, keeping tool-call payloads manageable.
- Python 3.11+
- uv (for dependency management & running)
- A Perseus API key (see docs.perseus.lettria.net)
- (Optional) Running Neo4j or FalkorDB instance if you want to use the database-export tools
git clone https://github.com/Lettria/perseus-mcp.git
cd perseus-mcp
uv sync # optional — uv run will do this on first launchuv sync creates .venv/, resolves uv.lock, and installs:
mcp[cli]— the Model Context Protocol Python SDKperseus-client— Lettria's Perseus SDKpython-dotenv— loads.envbefore the SDK reads settings
Running it upfront is not required — the MCP client (or any later
uv run) will auto-sync on first use — but it's a faster way to surface
dependency resolution errors.
The Perseus SDK reads its settings from environment variables (via pydantic-settings). You have two equivalent ways to supply them; pick whichever fits how you launch the server.
At minimum, PERSEUS_API_KEY must be set. All other variables are optional
— see sample.env for the full list (Neo4j, FalkorDB, inline-content
cap, log level).
Every MCP client lets you set environment variables on the subprocess it spawns. This keeps the credential attached to the server registration, with no extra file to manage. Examples are in the Running section below.
Useful during development when you uv run the server directly, or when
you don't want secrets inside a client config file:
cp sample.env .env
$EDITOR .envProcess environment variables always win over .env values, so a key set
in the MCP client config will override one sitting in .env.
Both options write your API key to a local file (either .env or the
client's JSON config). Treat them the same: don't commit them, and don't
sync them through cloud-backed config directories you wouldn't trust with
a password.
You do not need to start the server manually. MCP clients spawn the
subprocess themselves using the command you register below — the uv
invocation in the client config is what actually launches the server each
time the client connects.
uv will auto-sync the lockfile on first launch, so even uv sync is
optional; running it once upfront just surfaces dependency errors earlier.
Pass the API key (and any optional DB creds) with -e KEY=value:
claude mcp add lettria-perseus \
-e PERSEUS_API_KEY=sk-perseus-... \
-- uvx --from git+https://github.com/Lettria/perseus-mcp.git \
--with 'perseus-client[all]' \
lettria-perseus-mcpAdd more -e flags for Neo4j / FalkorDB if you plan to use those export
tools.
Why
--with 'perseus-client[all]'? Theneo4jandfalkordbPython drivers are declared as optional extras onperseus-client. Without[all](or the narrower[neo4j]/[falkordb]),save_graph_to_neo4jandsave_graph_to_falkordbfail at import time. Drop the--withflag if you only need TTL / Cypher exports.
Add an entry to claude_desktop_config.json and set credentials via the
env block:
Any other MCP client that can spawn a stdio subprocess supports the same
pattern — consult its docs for the equivalent of env / -e.
These are only useful for local debugging — not prerequisites for the MCP client registrations above.
Smoke-test that the server starts and auth succeeds:
PERSEUS_API_KEY=sk-perseus-... uv run lettria-perseus-mcp
# Ctrl-C once you see it waiting on stdio.Interactively call each tool via the MCP Inspector:
PERSEUS_API_KEY=sk-perseus-... uv run mcp dev src/lettria_perseus_mcp/server.pyTools are grouped below by concern. Every tool that returns graph data uses
graph_id — a short identifier minted when build_graph or
interlink_graphs succeeds and held in server memory for the lifetime of
the process.
| Tool | Description |
|---|---|
build_graph(file_paths, ontology_path?, refresh_graph?, metadata?) |
Build one graph per input file. Returns a summary and graph_id for each. |
build_graph_from_text(content, name?, ontology_ttl?, refresh_graph?, metadata?) |
Inline variant — pass document text (and optional TTL ontology) as strings. Bounded by PERSEUS_INLINE_CONTENT_MAX_BYTES. |
interlink_graphs(graph_ids, interlinking_key_uris?, immutable_properties?, merge_properties_on_conflict?) |
Merge two or more in-memory graphs on shared entity keys. |
| Tool | Description |
|---|---|
list_local_graphs() |
List every graph currently held in memory. |
get_graph_summary(graph_id) |
Entity / relation / document counts for one graph. |
get_graph_entities(graph_id, offset=0, limit=50) |
Paginated entity dump. |
get_graph_relations(graph_id, offset=0, limit=50) |
Paginated relation dump. |
forget_graph(graph_id) |
Drop a graph from the registry. |
forget_all_graphs() |
Drop all graphs from the registry. |
| Tool | Description |
|---|---|
export_graph_ttl(graph_id, output_path) |
Write the graph as Turtle. |
export_graph_cql(graph_id, output_path, strip_prefixes=True) |
Write the graph as Cypher statements. |
save_graph_to_neo4j(graph_id, strip_prefixes=True) |
Push to Neo4j via NEO4J_* env vars. |
save_graph_to_falkordb(graph_id, strip_prefixes=True) |
Push to FalkorDB via FALKORDB_* env vars. |
| Tool | Description |
|---|---|
upload_file(file_path, wait=True) |
Upload a source document; optionally block until processed. |
list_files(ids?, source_hashes?) |
List remote files. |
get_file(file_id) |
Fetch a single file record. |
delete_file(file_id) |
Delete a remote file. |
| Tool | Description |
|---|---|
upload_ontology(ontology_path, wait=True) |
Upload an ontology (TTL/OWL). |
upload_ontology_from_text(content, name?, wait=True) |
Inline variant — pass a TTL string directly. Bounded by PERSEUS_INLINE_CONTENT_MAX_BYTES. |
list_ontologies(ids?, source_hashes?) |
List remote ontologies. |
get_ontology(ontology_id) |
Fetch a single ontology record. |
delete_ontology(ontology_id) |
Delete a remote ontology. |
| Tool | Description |
|---|---|
submit_job(file_id, ontology_id?) |
Kick off extraction for an uploaded file. |
get_job(job_id) |
Fetch a single job (includes status). |
list_jobs(job_ids) |
Fetch multiple jobs at once. |
find_latest_job(file_id, ontology_id?) |
Most recent job for a file. |
find_latest_succeeded_job(file_id, ontology_id?) |
Most recent successful job. |
run_job(job_id, polling_interval=5, timeout=3600) |
Block until a job reaches a terminal state. |
download_job_output(job_id, output_path?) |
Save a job's output JSON locally. |
An agent can chain:
build_graph(file_paths=[...])→ gathergraph_idsinterlink_graphs(graph_ids=[...])→ single merged graphexport_graph_ttl(graph_id, "output/merged.ttl")
- Ensure
NEO4J_URI/NEO4J_USER/NEO4J_PASSWORDare set in.env. build_graph(...)save_graph_to_neo4j(graph_id)
list_files()/list_ontologies()find_latest_succeeded_job(file_id, ontology_id?)download_job_output(job_id, "output/job.json")
Some MCP clients (notably Claude Desktop) cannot write files to disk on their own, which breaks workflows where the agent drafts an ontology or document in chat and then wants Perseus to ingest it. To avoid forcing users to install a separate filesystem MCP server for that common case, the server provides inline variants:
build_graph_from_text(content, ...)— document text as a string.upload_ontology_from_text(content, ...)— TTL ontology as a string.
Both write the payload to a temp file, call the SDK, and clean up before returning. No path management required from the agent.
Inline payloads are capped at PERSEUS_INLINE_CONTENT_MAX_BYTES
(default 1 MiB / 1,048,576 bytes, measured as UTF-8). Anything larger
is rejected with an error message that lists the three alternatives:
- Save locally and use the path-based tool
(
build_graph/upload_ontology). - Register the official
@modelcontextprotocol/server-filesystemserver and let the agent write the file itself. - Raise the cap via
PERSEUS_INLINE_CONTENT_MAX_BYTESif you accept the memory / MCP-transport overhead.
The cap is deliberately conservative because every byte of inline content crosses the MCP wire and sits in the model's context. For real-world corpora (PDFs, multi-megabyte exports), path-based ingestion is always the right answer.
- Graph state is in-memory. Restarting the server discards the registry;
export anything you want to keep. This is deliberate —
KnowledgeGraphobjects are Pydantic models that aren't cheap to serialize, and MCP servers are meant to be short-lived helpers. - Large graphs use pagination.
get_graph_entities/get_graph_relationsdefault to slices of 50 and returntotalso an agent can loop. - Secrets stay in env vars. API keys and database credentials are never passed through tool arguments; the SDK reads them via pydantic-settings.
- Failures surface as tool errors.
PerseusExceptions and validation errors propagate up to the MCP client rather than being swallowed.
Automated checks run on the repository so the published package stays auditable:
- Dependabot — security-vulnerability alerts and automated fix PRs, plus
weekly version-update PRs for the
uv-resolved Python dependencies and the GitHub Actions used in CI (.github/dependabot.yml). - Secret scanning — GitHub secret scanning with push protection, so credentials can't be committed. No secrets live in the repo: API keys and database passwords are read from the environment at runtime only (see Configuration).
- SAST — CodeQL static analysis on every
push/PR to
mainand weekly, using thesecurity-and-qualityquery suite (.github/workflows/codeql.yml). - License scan — a dependency-license inventory plus GitHub
dependency-review, which blocks PRs that introduce strong-copyleft or
vulnerable dependencies (
.github/workflows/license-scan.yml).
Found a vulnerability? Please report it privately via the repository's security advisories rather than opening a public issue.
# Install + create virtualenv
uv sync
# Run the server
PERSEUS_API_KEY=... uv run lettria-perseus-mcp
# List registered tools without starting stdio
PERSEUS_API_KEY=test uv run python -c "
from lettria_perseus_mcp.server import mcp
for t in mcp._tool_manager.list_tools():
print(t.name)
"Tests mock the perseus_client SDK so no API key or network is needed.
# Install dev dependencies (pytest + pytest-asyncio)
uv sync --group dev
# Run the full suite
uv run pytest tests/ -vtests/
├── fakes.py # Lightweight stand-ins for SDK models
│ (FakeOntology, FakeKnowledgeGraph, etc.)
├── conftest.py # mock_perseus fixture — patches all SDK calls
├── test_helpers.py # _safe_basename, _inline_max_bytes,
│ _check_inline_size, graph registry
├── test_ontology_upload.py # upload_ontology, upload_ontology_from_text,
│ list/get/delete ontologies
└── test_graph_tools.py # build_graph, build_graph_from_text,
interlink, local registry, exports
| Area | Examples |
|---|---|
| Ontology upload (TTL) | Temp file content, filename sanitization, .ttl extension appending, size-limit enforcement, wait/no-wait, cleanup |
| Graph building | Path-based and inline-text variants, inline ontology pass-through, size limits |
| Helpers | Path traversal in _safe_basename, env-var validation, multibyte UTF-8 size checks |
| Graph registry | Register/require round-trip, unknown-ID errors, pagination, forget/forget-all |
| Exports | TTL and CQL file writing |
| DB saves | Neo4j / FalkorDB env-var preconditions, strip_prefixes forwarding, missing-driver hint in error |
| CRUD tools | list/get/delete for ontologies, files |
MIT — see LICENSE for details.
- Perseus docs: https://docs.perseus.lettria.net/
perseus-clienton PyPI: https://pypi.org/project/perseus-client/- Model Context Protocol: https://modelcontextprotocol.io/
{ "mcpServers": { "lettria-perseus": { "command": "uvx", "args": [ "--from", "git+https://github.com/Lettria/perseus-mcp.git", "--with", "perseus-client[all]", "lettria-perseus-mcp" ], "env": { "PERSEUS_API_KEY": "sk-perseus-...", // Optional: only if you use `save_graph_to_neo4j`. These vars alone // are not enough — the `--with 'perseus-client[all]'` flag above is // what installs the `neo4j` driver into the uvx environment. "NEO4J_URI": "bolt://localhost:7687", "NEO4J_USER": "neo4j", "NEO4J_PASSWORD": "password", // Optional: only if you use `save_graph_to_falkordb`. Same caveat — // requires the `--with` flag above so the `falkordb` driver is // installed alongside `perseus-client`. "FALKORDB_HOST": "localhost", "FALKORDB_PORT": "6379", "FALKORDB_USERNAME": "", "FALKORDB_PASSWORD": "", "FALKORDB_GRAPH_NAME": "perseus_graph" } } } }