Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion .specify/memory/constitution.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
<!--
Sync Impact Report
==================
Version: 2.0.0 -> 2.0.1 (2026-10-14)

Modified: Principle IV, wording -- records that the `extract-schema` operator
command connects without MCP_ALLOW_CYPHER, and why that is not an
instance with graph access.
Added: none
Removed: none

Rationale: PATCH. No principle changed: the gate is about what an instance
publishes, and the CLI publishes nothing. Written down because the principle
says a new entrypoint belongs in the gate matrix, and this one deliberately
sits outside it.

Previous: 2.0.0 (2026-10-14), below.

Version: 1.0.0 -> 2.0.0 (2026-10-14)

Modified: Principle IV redefined -- "no Neo4j connection in any deployment"
Expand Down Expand Up @@ -93,6 +108,15 @@ was restored for internal use, on a different footing:
and the registrars in every combination of the two variables and counts
driver constructions. A new entrypoint belongs in that matrix.

**Not an instance.** `reactome-mcp extract-schema` (`src/cli/extract-schema.ts`)
opens the gate for its own process and nothing else: an operator runs it by
hand against a database named on its command line, it writes that database's
schema to a file, and it serves nothing -- it imports no server, tool, resource
or transport, which the gate test asserts. It still connects through
`getDriver()`, in READ sessions under the schema timeouts. A saved schema
(`NEO4J_SCHEMA_FILE`) is read only behind the gate, and served only against
the database whose fingerprint it carries.

What would reverse this: graph access being needed on a public instance, or the
gate failing open anywhere. Either means removing the capability again, not
adding another check.
Expand Down Expand Up @@ -168,4 +192,4 @@ Amendments bump the version: MAJOR for a principle removed or redefined, MINOR
for one added, PATCH for wording. Specs live in `specs/`; the ones that record
why something is the way it is are as valuable as the ones that plan what is next.

**Version**: 2.0.0 | **Ratified**: 2026-09-14 | **Last amended**: 2026-10-14
**Version**: 2.0.1 | **Ratified**: 2026-09-14 | **Last amended**: 2026-10-14
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ All notable changes to this project are documented here. This project adheres to

`NEO4J_DATABASE` now defaults to the server's default database, and `NEO4J_PASSWORD` to none (connect without credentials), rather than `graph.db` and `neo4j`.

- **Save the graph schema, and serve it only against the database it came from.** `reactome-mcp extract-schema --uri … [--database] [--user] [--password-env VAR] [--out file]` runs the server's own extractor once and writes the schema JSON. It is an operator command, not an instance, so it does not need `MCP_ALLOW_CYPHER`; it reads in READ sessions under the same timeouts, refuses a password on the command line (it would show in `ps`) and takes it from the environment, and refuses to write a schema with unavailable sections. Every schema now carries `schemaFormat` (2) and a `source` fingerprint: Neo4j version and edition, the connected database's name, the Reactome release from `DBInfo` (absent where there is none), total node and relationship counts, digests of the label, relationship type and property key lists and of the indexes and constraints, and when it was taken. `NEO4J_SCHEMA_FILE` serves a saved schema instead of extracting at startup, but only after the live database gives the same fingerprint back (metadata and count-store queries, not the extraction) and the file agrees with its own fingerprint; anything else — another release or database, a changed count, a new label, type or property key, a changed index or constraint, another format, a missing, malformed or self-contradictory file — is warned about by name and the schema is extracted live. Changes that only touch property values or their types are not detected; the README says so. The CLI never echoes an argument value in a usage error, and refuses to write a schema whose body disagrees with its fingerprint (the database changed during the run). With graph access off the file is never read. The schema digest names the Reactome release. The README covers pointing an internal instance at your own database, and producing `reactome-<release>-schema.json` in the release pipeline.

- **`MCP_TOOL_GROUPS` — choose which tools an instance publishes.** All 59 register by default, so a local stdio user is unaffected; a hosted instance names the groups it means to serve (`search`, `pathway`, `entity`, `analysis`, `export`, `interactors`, `gsa`, `utilities`).

It is a **registration** switch, not a documentation one. An omitted group's tools are not registered, do not appear in `tools/list`, are not mentioned in the instructions the server sends on connection, and neither are its **resources** — omitting `analysis` withholds `reactome://analysis/{token}` as well, since a capability moved to a URI is not a capability withheld. A tool described nowhere but still answering is the divergence that let this server advertise Cypher tools it had not registered.
Expand All @@ -36,6 +38,7 @@ All notable changes to this project are documented here. This project adheres to
- **`reactome_query` returned pretty-printed JSON**, which cost ~23% more tokens for whitespace no model needs. Objects too large to return whole are now described — field names, shapes and sizes, with a pointer to the `attribute` argument — instead of being truncated into invalid JSON. Metabolism goes from 43,457 characters of severed object to 1,327 characters of usable map.
- **`events_hierarchy` failed every call that did not name a species.** Its default was `"Homo sapiens"`, which that endpoint answers with HTTP 500; `"9606"` returns 200.
- A failed Neo4j driver close rejected with nobody listening — `process.once` was handed an `async` function.
- Every Neo4j driver build added its own `SIGINT`/`SIGTERM`/`beforeExit` listeners and nothing removed them, so a process that closed and reconnected accumulated them past Node's leak warning. They are added once per process.
- `fetchWithRetry` rethrew `lastError`, typed `unknown`, so a non-`Error` rejection reached callers as something they could not read `.message` off.

### Added
Expand Down
111 changes: 111 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ Optional:
| `NEO4J_PASSWORD` | _(unset)_ | unset connects without credentials, for a database with authentication disabled |
| `NEO4J_DATABASE` | _(server default)_ | Neo4j Community serves a single database, so this is rarely needed |
| `CYPHER_QUERY_TIMEOUT_MS` | `30000` | server-side transaction timeout for `reactome_cypher_*` queries |
| `NEO4J_SCHEMA_FILE` | _(unset)_ | a schema saved by [`extract-schema`](#saving-the-schema-extract-schema), served instead of extracting one when it matches the database; ignored entirely while graph access is off |

**Read-only safeguards.** Every query runs in a READ-mode session, so the
database refuses `CREATE`, `MERGE`, `DELETE`, `SET` and `REMOVE`. Before a
Expand Down Expand Up @@ -158,6 +159,116 @@ is read once in the background at startup and cached; on the full graph that
can take a while, and a section the database cannot provide is reported as
unavailable rather than shown empty.

### Saving the schema: `extract-schema`

Reading the schema of the full Reactome graph takes about a minute. To skip
that at startup, save it once and point the server at the file:

```bash
NEO4J_PASSWORD=… node dist/index.js extract-schema \
--uri bolt://127.0.0.1:7687 --out reactome-schema.json
# installed from npm: npx reactome-mcp extract-schema …

NEO4J_URI=bolt://127.0.0.1:7687 MCP_ALLOW_CYPHER=1 \
NEO4J_SCHEMA_FILE=reactome-schema.json node dist/index.js
```

| Option | |
|---|---|
| `--uri <uri>` | the database (required) |
| `--database <name>` | default `NEO4J_DATABASE`, else the server's default database |
| `--user <name>` | default `NEO4J_USER`, else `neo4j` |
| `--password-env <VAR>` | read the password from `VAR` instead of `NEO4J_PASSWORD` |
| `--out <file>` | write here; without it the JSON goes to stdout |

There is no `--password`, and passing one is refused: a command line is visible
to every user of the machine in `ps`. The password comes from the environment.

`extract-schema` runs the server's own extractor, read-only and under the same
timeouts, so the file has exactly the structure of the `reactome://graph/schema`
resource. It does not need `MCP_ALLOW_CYPHER` — it is a command an operator runs
by hand, not a server, and it publishes nothing. It refuses to write a schema
with sections it could not read, since a saved schema would keep that gap for as
long as it is used, or one whose body disagrees with its own fingerprint, which
means the database was written to during the run. Usage errors name the flag at
most and never echo a value.

**The file is only served against the database it came from.** Every schema
carries a `schemaFormat` number (currently `2`) and a `source` fingerprint:

- Neo4j version and edition, and the name of the database actually connected to
- the Reactome release from the graph's `DBInfo` node (absent on a graph without one)
- total node and relationship counts
- digests of the label, relationship type and property key lists, and of the
indexes (name, labels, properties, state) and constraints
- when it was taken (recorded, not compared)

On its first schema request the server reads the same fingerprint from the live
database — metadata procedures and count-store lookups, nothing that scans the
nodes or their properties, rather than the full extraction — and serves the
file only if every field agrees, the format is one it reads, and the file agrees
with its own fingerprint. When it does, it logs `graph schema served from file`
with the file, release and database. Otherwise it logs a warning naming the
difference and extracts the schema live, as if no file were set. With graph
access off, `NEO4J_SCHEMA_FILE` is not read at all.

| Detected (the file is not used) | Not detected (the file is still used) |
|---|---|
| another release, database or Neo4j version | a property's value changing |
| a node or relationship added or removed | a property's value type changing (say, a string becoming a list) |
| a new label, relationship type or property key (`SET n.newProp`) | an existing property key set on nodes that lacked it |
| an index or constraint created, dropped or no longer online | one node deleted and another added with the same labels, keys and totals |
| an older format, or a missing, malformed or self-contradictory file | a property key no longer used anywhere — Neo4j keeps key tokens once created |

So a snapshot is safe against a different database and against most structural
edits, but it is a fingerprint, not a proof: after edits that only change
values, re-run `extract-schema` or unset `NEO4J_SCHEMA_FILE`.

**From the release pipeline.** Once a release's graph database image is built,
the same command against a container started from it produces a schema that can
be published beside the image as an optional artifact:

```bash
docker run -d --name graphdb-schema -p 127.0.0.1:7687:7687 <the new graph database image>
node dist/index.js extract-schema --uri bolt://127.0.0.1:7687 \
--out reactome-<release>-schema.json
docker rm -f graphdb-schema
```

An instance run against that image can then set `NEO4J_SCHEMA_FILE` to the
published file; one run against anything else will fall back to live extraction
on its own.

### Pointing at your own database

A curator, or anyone with a Reactome-shaped graph of their own, can run an
internal instance against it. The same rule applies as everywhere else here:
**only on an instance that is not publicly reachable** — on your own machine
over stdio, or bound to loopback.

```bash
NEO4J_URI=bolt://127.0.0.1:7687 \
NEO4J_USER=neo4j NEO4J_PASSWORD=… \
NEO4J_DATABASE=<your database, if not the default> \
MCP_ALLOW_CYPHER=1 \
node dist/index.js
```

Nothing else is needed: the schema the Cypher tools describe is read from your
database at startup, not shipped with the server, so it reflects your labels,
properties and counts. A database without a `DBInfo` node is fine; the schema
simply names no release.

To skip the extraction on later starts, save your database's schema with
`extract-schema` and set `NEO4J_SCHEMA_FILE` to it. The fingerprint check is
what makes this safe to get wrong: a schema saved from the release graph will
not match your database, so it is not used against it, and nor is your own
snapshot once your database has changed in a way the fingerprint sees — a node
or relationship added or removed, a new label, relationship type or property
key, an index or constraint changed. In those cases the server warns and reads
the schema live. Edits that only change property values or their types are
**not** seen (see the table above); after those, re-run `extract-schema`.

## Usage

### With Claude Desktop
Expand Down
Loading
Loading