Skip to main content
Glama
jeffneo

neo4j-mcp-gateway

by jeffneo

Neo4j MCP Gateway

A single local MCP gateway for Neo4j. Run it once, connect from VS Code and Claude Desktop, and get two categories of tools behind one stdio endpoint:

  1. Generic queryingproxied from the official neo4j/mcp server (schema introspection + read/write Cypher + GDS). These are not reimplemented: the gateway spawns the supported server as a downstream child and re-exposes its tools unchanged (get-schema, read-cypher, write-cypher, list-gds-procedures).

  2. Use-case tools — parameterized, purpose-built tools defined as YAML files, shipped in swappable bundles (bundles/<name>/tools/). Adding one is: drop in a new *.yaml and restart. They run their own parameterized Cypher and are namespaced (usecase_*) so they never collide with the proxied tools.

The point: keep the official, supported server intact for generic work, while making it trivial to add and iterate curated use-case tools.

        ┌──────────────────────── neo4j-mcp-gateway (this repo) ─────────────────────────┐
        │                                                                                 │
 VS Code│  ┌───────────────┐   mount    ┌──────────────────────────────┐  stdio (child)  │
 Claude ─┼─▶│ FastMCP server │◀──────────│ FastMCP proxy (create_proxy) │─────────────────┼─▶ official neo4j/mcp
 Desktop│  │  (stdio)       │            └──────────────────────────────┘                 │   (uvx / docker / binary)
 (stdio)│  │                │   add_tool ┌──────────────────────────────┐  bolt           │
        │  │                │◀──────────│ YAML tools (neo4j driver)     │─────────────────┼─▶ Neo4j
        │  └───────────────┘            └──────────────────────────────┘                 │
        └─────────────────────────────────────────────────────────────────────────────────┘

Bundles (swappable use cases)

The gateway/ package is a domain-agnostic engine. Everything use-case-specific lives in a bundle — a self-contained folder under bundles/:

bundles/<name>/
  bundle.yaml     # metadata only (name, description, model instructions,
                  #   REQUIRED security.mode, downstream.*, tool prefix) — NO secrets
  .env            # optional, git-ignored: this bundle's Neo4j connection override
  tools/*.yaml    # static parameterized use-case tools
  pytools/*.py    # optional code-backed tools (build_tools(ctx) -> [Tool]) for
                  #   logic that isn't static Cypher
  data/*.cypher   # demo dataset generator(s) + demo docs

Pick the active bundle with ACTIVE_BUNDLE (or --bundle). The engine points the tools, data path, downstream connection, and the server's model-facing instructions at that bundle.

Connection is env-only — URI / username / password / database are read from the root .env, then a bundle's git-ignored .env overrides them. Nothing connection-related lives in bundle.yaml, so a bundle can target an entirely separate Neo4j instance (e.g. a different Aura) with no secrets in committed files.

uv run neo4j-mcp-gateway --list-bundles          # ato, iam, …
ACTIVE_BUNDLE=iam uv run neo4j-mcp-gateway         # serve a specific bundle
uv run python scripts/new_bundle.py <name>         # scaffold a new bundle
ACTIVE_BUNDLE=<name> uv run python scripts/validate_bundle.py   # run all its tools

Serving several bundles at once

ACTIVE_BUNDLE accepts a comma list. Each bundle keeps its own connection, so they may sit on different databases or entirely different Neo4j instances:

ACTIVE_BUNDLE=ato,iam uv run neo4j-mcp-gateway     # or --bundle ato,iam

Tools are then namespaced per bundle (ato_mule_hubs, iam_client_activity, iam_secure-read-cypher), and each bundle keeps its own security posture — in the example above ato_read-cypher stays available while iam_read-cypher is hidden. Bundles sharing a datasource share one downstream official server rather than spawning a redundant child. A single bundle behaves exactly as before (no bundle prefix), so nothing changes unless you opt in.

Safety rule: the gateway refuses to start if an open bundle and a mediated bundle resolve to the same database. The open bundle's unfiltered tools would read the very rows the mediated bundle protects, and no amount of tool-hiding fixes that. Give them separate databases, make both mediated, or run separate gateway processes.

Two costs worth knowing: every bundle's instructions are concatenated (which dilutes tool selection — keep it to a handful of bundles), and each distinct datasource spawns its own downstream child.

Swap without editing files — register one client entry per bundle, each pinned via env:

"mcpServers": {
  "neo4j-ato": { "command": "uv", "args": ["run","--directory","/ABS/PATH","neo4j-mcp-gateway"], "env": {"ACTIVE_BUNDLE":"ato"} },
  "neo4j-iam": { "command": "uv", "args": ["run","--directory","/ABS/PATH","neo4j-mcp-gateway"], "env": {"ACTIVE_BUNDLE":"iam"} }
}

Access mode is a required declaration

Every bundle.yaml must state security.mode — there is no default, so "unfiltered" is a recorded decision rather than something that happens by omission:

security:
  mode: open        # tools read directly (all consumers uniformly entitled)
  # mode: mediated  # every read is entitlement-filtered against the caller

Under mediated, the engine:

  1. registers resolve-identity and (optionally) secure-read-cypher — entitlement mediation is an engine capability, so no bundle ships security code;

  2. wraps every curated YAML tool in the authorization prelude + entitlement filter, so the same tool returns different rows per caller;

  3. auto-hides raw read-cypher, which would bypass the filter, and defaults the downstream to read-only;

  4. requires tools to use the mediated authoring form (below) and to be read-only.

Mediated tools declare the split explicitly rather than having the engine parse Cypher to find the RETURN — getting that wrong would be a security bug:

match: |                # no RETURN
  MATCH (t:Trade)-[:FOR_CLIENT]->(c:Client {name: $client})
scope: [t, c]           # variables carried into the return; ALL are filtered
protect: [t]            # optional: strict — must carry an ACL or the row is dropped
return: |               # runs AFTER filtering, so aggregates are per-entitlement
  RETURN t.tradeId AS tradeId, t.notional AS notional

Postures. A mediated bundle can publish curated tools only by setting security.expose_open_query_tool: false (or EXPOSE_OPEN_QUERY_TOOL=false at runtime). The open-ended secure-read-cypher is then never registered, so no Cypher is generated at runtime — the stance for regulated workflows. The env variable can only tighten: a bundle declaring curated-only cannot be re-opened from the shell.

Denials withdraw access someone otherwise holds. A restricted list is not the absence of a grant — coverage still covers the client — so it cannot be modelled by deleting one. Grants and denials share a shape: via (a path from the caller), where (a condition on the row), or both.

grants:
  - label: Trade
    via: "(caller)-[:ON_DESK]->(:Desk)<-[:BOOKED_ON]-(resource)"
    where: "resource.notional < 50000000"     # a condition on the ROW
denials:
  - label: Trade
    where: "resource.restricted = true"
    reason: "the trade is flagged restricted"

The test is (granted) AND NOT (denied)deny always wins, and explain-access reports which grants matched but were overridden. A denial whose predicate is NULL does not fire, because an absent property yields NULL and absence is not ambiguity; where absence should deny, write coalesce(resource.clearance, 0) < 3.

Which edges decide access? Computed, not asserted:

uv run python scripts/entitlement_surface.py asset_platform

An entitlement edge is not a kind of edge — AUTHORED_BY is a business fact until a grant traverses it. So the entitlement graph is the projection of the graph onto the types and properties named in grants, denials and identity.

The split that decides how each edge can be governed is who writes it, which is a deployment fact rather than a rule fact — so it is declared once in security.ingested_rels. Anything absent from that map is authored, and a role can be denied write on it outright:

uv run python scripts/entitlement_surface.py asset_platform --write-guard business_feed

In asset_platform that is 4 edges out of 22. The other 18 are written by a feed, so a routine upstream edit moves access and the privilege cannot be taken away without breaking ingestion. A parallel "entitlement-only" copy of them does not help — it moves the same edit one step downstream and adds a staleness failure nothing in the query can detect. Mint a separate type when you need to revoke someone's ability to write it, not to tidy the diagram; full argument in docs/entitlement-edges.md.

The report also flags a rule whose relationship type is absent from the graph — for a grant that under-grants, but for a denial it fails open — and prices every denial for barrier coupling: a denial can be lifted while a grant survives whenever it traverses a feed-written edge that grant does not. Nothing is missing from anyone's results when that happens, so no per-caller case and no edge-presence invariant catches it. The only structurally total barrier is a where-only denial, which traverses nothing.

Thresholds are not principals. rankLevel >= 5 is an ordering, and a set of names cannot express one without minting a principal per rank. Declare identity.caller_attributes: [rankLevel] and a rule reads authz.attrs.rankLevel >= 5, resolved once in the prelude. A scalar crosses a separated-identity boundary where the caller node cannot, so thresholds work unchanged under all three identity.source values.

Declare protected_labels so scripts/validate_bundle.py fails when a business record is missing its access-control list — otherwise such a record silently flows to everyone. The validator also persona-diffs mediated tools to prove the filter actually discriminates between callers.

New to this? docs/entitlement-model-brief.md explains the model conceptually in about three minutes. Full reference and known limits: docs/mediation-spec.md. What each entitlement model costs the data pipeline — what must be ingested, by whom, and what breaks when it is late: docs/data-ingestion.md.

Note: get-schema stays exposed even under mediated, because text-to-Cypher needs it. It reveals structure (labels, relationship types, property keys) but no row data. Add it to downstream.hide if your deployment treats the schema itself as sensitive.

Audit logging

Set a path and every tool call appends one JSON object — who called, as whom, which tool, in which bundle, the outcome, and how many rows survived the filter:

NEO4J_MCP_AUDIT_LOG=/var/log/neo4j-mcp/audit.jsonl
NEO4J_MCP_AUDIT_ARGUMENTS=true    # optional: also record argument VALUES
{"ts":"2026-08-18T19:18:36.407+00:00","event":"tool_call","tool":"client_opportunities",
 "bundle":"asset_platform","mode":"mediated","identitySource":"graph","grantModel":"both",
 "principal":"evan.brooks@bank.com","principalSource":"impersonation-request",
 "impersonated":true,"argumentNames":["client"],"durationMs":18.1,"outcome":"ok","rows":1}

Row contents are never logged. An audit log that copies the rows it audits is a second, less-protected replica of the data the filter exists to restrict — usually on a filesystem with weaker controls, often shipped to an aggregator a different team can read. The record carries the row count and nothing about the rows. Argument values are the judgement call and are off by default; argument names are always recorded, so you can see which question was asked without its subject.

impersonated is top-level rather than something to infer: running as another principal is a privileged action and is the first thing a reviewer looks for. Proxied tools are covered too, so an open bundle's raw read-cypher is audited on the same terms — and a call to a hidden tool is recorded as a rejection.

A bundle can declare security.require_audit: true, and the gateway then refuses to start without a log path — the same fail-closed stance as security.mode.

Records are hash-chained (seq, prev, hash), so editing, deleting or reordering a line is detectable:

uv run python scripts/verify_audit.py audit.jsonl --checkpoints checkpoints.jsonl

A chain proves tampering only if its head exists somewhere the log's writer cannot rewrite — otherwise the file can be truncated and restarted and will verify clean. NEO4J_MCP_AUDIT_FORWARDER publishes (seq, head) periodically to such a place. stderr and file:<path> ship for development; a deployment registers its SIEM or WORM sink with gateway.audit.register_forwarder() — a one-method contract. Startup warns when nothing anchors the chain.

Where identity lives

By default the identity graph sits beside the data and the prelude traverses it in the same statement. security.identity.source moves it:

security:
  identity:
    source: graph        # default — identity beside the data, one statement
    # source: composite  # identity and data in separate databases, joined by a
    #   identity_graph: fed.identity        #   composite database. Still ONE
    #   data_graph: fed.data                #   statement and one transaction.
    # source: remote     # identity resolved over a SECOND connection, from
    #   remote_env_prefix: IDENTITY         #   IDENTITY_NEO4J_URI etc. in .env

composite and remote make the identity store independent — its own instance, credentials and lifecycle, shareable across domains. Neither gives the data query a caller node, so both forbid anchoring and tools that reference caller.

Path grants survive both, because a grant does not need the caller node — it needs a value derived from the caller. A relationship cannot span two databases, but a traversal can be cut at a node present in both: Neo4j's documented proxy node pattern. The engine finds that cut itself and re-roots the data-side half at the proxy, so patterns are authored once and mean the same thing co-located or split. A grant that cannot be cut safely — an identity relationship appearing after the boundary, which would deny silently — is rejected at load. See GRANT_SPLITTING in gateway/mediation.py.

composite

remote

Path grants (grant_model: path / both)

Anchoring

Round trips

1 (one statement, one transaction)

2, with a consistency window

Needs a composite database

yes

no — any two connections

Proxy nodes in the data database

required

required

Anchors split by the same rule, so the performance lever survives too — measured at ~17x either side of the split on 100,000 rows (scripts/bench_separation.py). What both give up is tools that reference caller in their match; express that scoping with an anchor or a parameter instead.

Cutting at a property instead of a proxy node removes the proxies entirely. Where the boundary is already recorded as a property — the covering team on the Client, the author on the Interaction — declare it and the grant compares that property instead of traversing to a proxy:

identity:
  boundary_properties:
    Client: coverageTeam
    Interaction: loggedByEmail

Measured against a database holding no proxy nodes at all, results were identical to co-located across 24 comparisons. Each grant also loses a hop, and a boundary property on the row itself collapses to a bare comparison with no subquery. See the recipe in docs/entitlement-testing-tutorial.md.

The property and the relationship are two recordings of one fact and can drift apart. Nothing detects that from the pattern alone, so keep a differential: conformance case proving they agree on real data.

Downstream identity: making native rules apply to the end user

Native database rules (RBAC, property rules, ABAC-assigned roles) are evaluated against the account that connects. A gateway holding one service connection gets them evaluated against the service account, so no per-user rule applies at all. Two ways to close that, per session:

NEO4J_MCP_ACCESS_TOKEN=<jwt>       # the caller's token authenticates the session
NEO4J_MCP_DB_IMPERSONATION=true    # service account impersonates the principal

With a token, the database validates it — signature, issuer, audience, expiry — and maps its claims to roles. The gateway never inspects it, which is the point: token validation belongs to something built for it. Setting both is refused, since a token already asserts who the caller is.

These compose with mediation rather than replacing it. Measured with a real PBAC rule (FOR (o:Opportunity) WHERE o.stage = 'Proposal') on a real native role:

Rows

service account — mediation only (her coverage)

2

impersonated — mediation ∩ PBAC

1

The caller sees the intersection, which is what a layered model should do.

One deployment note found the hard way: with identity co-located, the impersonated user also needs read access to the identity graph, or the authorization prelude resolves nothing and every query returns zero rows. It fails closed, but it looks like an entitlement bug. identity.source: remote avoids it — identity resolution uses its own connection and only the data query is impersonated.

Adding another source (an external entitlement service, LDAP, token introspection) means implementing IdentitySource and registering it — see gateway/identity_sources.py.

Want to try the entitlement model on Aura? docs/entitlement-testing-tutorial.md walks through it across six identity/data topologies.

Shipped bundles: ato (account-takeover; 7 YAML tools; mode: open), asset_platform (sector-classified asset universe; taxonomy-scoped entitlement, two caller classes, dated scopes; the reference model) and iam (investment-bank entitlements; mode: mediated, curated tools filtered per caller, raw read-cypher auto-hidden). Neither bundle contains security code — a bundle declares a policy and the engine enforces it.


Related MCP server: @zhangzwd/mcp-gateway

Prerequisites

  • Python 3.11+

  • uv (brew install uv / pipx install uv)

  • A reachable Neo4j instance (local, Docker, or Aura) with credentials

  • The official downstream server is fetched automatically on first run via uvx neo4j-mcp-server — no manual install. (Docker / a built Go binary also work; see .env.example.)

Note: the official server verifies Neo4j connectivity at startup and exits if it cannot connect. If your credentials are wrong or Neo4j is unreachable, the proxied get-schema / *-cypher tools will not appear — check the gateway's stderr log. The YAML use-case tools still load regardless and report connection problems as clean per-call errors.


Setup

# from the project root
cp .env.example .env
# edit .env with your Neo4j URI / user / password / database
uv sync

.env (git-ignored) holds the real credentials. The same credentials flow to both the downstream official server and the YAML tool executor.

Variable

Default

Purpose

NEO4J_URI

bolt://localhost:7687

Neo4j bolt URI (shared)

NEO4J_USERNAME

neo4j

Neo4j user (shared)

NEO4J_PASSWORD

password

Neo4j password (shared)

NEO4J_DATABASE

neo4j

Target database (shared)

NEO4J_MCP_CMD

uvx neo4j-mcp-server

How to launch the official downstream server

NEO4J_READ_ONLY

(unset)

true disables downstream write-cypher

NEO4J_TELEMETRY

false

Downstream telemetry opt-in

ACTIVE_BUNDLE

ato

Bundle(s) to serve — comma list for several at once

EXPOSE_OPEN_QUERY_TOOL

(bundle)

Set false to drop secure-read-cypher (tighten only)

USECASE_PREFIX

usecase_

Tool-name prefix (also settable in bundle.yaml)

Tools and data come from the active bundle (bundles/<ACTIVE_BUNDLE>/); the database and model instructions can be declared in its bundle.yaml.


Run

uv run neo4j-mcp-gateway
# equivalent:
uv run python -m gateway.server

The gateway serves over stdio — that's what editors launch. On startup it logs (to stderr) the downstream command, the mounted official tools, and the YAML use-case tools it registered.

Verify with the MCP Inspector

# List the union of tools (official proxied + YAML use-case)
npx @modelcontextprotocol/inspector --cli uv run neo4j-mcp-gateway --method tools/list

# Call a generic proxied tool
npx @modelcontextprotocol/inspector --cli uv run neo4j-mcp-gateway \
  --method tools/call --tool-name get-schema

# Call a YAML use-case tool
npx @modelcontextprotocol/inspector --cli uv run neo4j-mcp-gateway \
  --method tools/call --tool-name usecase_ato_session_triage --tool-arg min_risk=5

Or launch the Inspector UI (drop --cli) and browse/click the tools.


Adding a use-case tool (the whole point)

  1. Create bundles/<active-bundle>/tools/my_tool.yaml (e.g. bundles/ato/tools/my_tool.yaml):

    name: recent_transactions_for_customer
    description: Recent transactions performed by a customer's accounts.
    parameters:
      - name: customer_id
        type: string
        description: Customer.customerId
        required: true
      - name: limit
        type: integer
        description: Max rows to return
        required: false
        default: 25
    cypher: |
      MATCH (c:Customer {customerId: $customer_id})-[:HAS_ACCOUNT]->(:Account)
            -[:PERFORMS]->(t:Transaction)
      RETURN t.transactionId AS id, t.amount AS amount, t.date AS date
      ORDER BY t.date DESC
      LIMIT $limit
    read_only: true   # set false to run in write mode
  2. Restart the gateway (see Restarting). It appears as usecase_recent_transactions_for_customer.

Tools are discovered once at startup and MCP clients cache the tool list, so a new/edited YAML file needs a restart to show up — saving alone is not enough.

Schema reference

Field

Required

Notes

name

Alphanumeric/underscore. Final tool name is <USECASE_PREFIX><name>.

description

Shown to the model.

parameters

List of {name, type, description, required, default}.

parameters[].type

string · integer · number · boolean · array · object (default string).

cypher

Parameters bind to $name placeholders.

read_only

true (default) → read transaction; false → write transaction.

Malformed files fail loudly at startup with a message naming the file. Results are returned as JSON: { "count": N, "records": [ ... ] }, with Neo4j temporal / spatial / graph values converted to JSON-friendly forms.

Fast dev loop — test a tool without MCP or a gateway restart:

uv run python scripts/try_tool.py --list
uv run python scripts/try_tool.py mule_hubs min_victims=2

scripts/try_tool.py runs one tool's Cypher through the same loader/executor the gateway uses, straight against Neo4j — so you get instant feedback while writing YAML. Use the MCP Inspector to check the tool over MCP, and a client (Claude Desktop) for the final integration.


Restarting to pick up new tools

Adding or editing a YAML tool requires a restart. The cleanest way depends on how the gateway is running:

  • In VS Code / Claude Desktop (normal use): don't kill it in a terminal — let the client restart it, which stops the process by closing its stdin (a clean, instant shutdown).

    • VS Code: open .vscode/mcp.json and click Restart on the server, or run MCP: List Servers → neo4j-gateway → Restart from the command palette.

    • Claude Desktop: toggle the connector off/on (or quit and reopen Claude).

  • Running it yourself in a terminal (e.g. testing with the Inspector): a single Ctrl+C stops it immediately — the gateway installs a fast SIGINT/SIGTERM handler that exits at once and lets the downstream child close via stdin-EOF, rather than blocking on an async teardown. kill <pid> (SIGTERM) works the same way.

If you ever see leftover neo4j-mcp-server processes from an earlier session:

pgrep -fl 'neo4j-mcp-server|neo4j-mcp-gateway'   # inspect first
pkill -f 'neo4j-mcp-server'                       # then clean up stale ones

Heads-up: pkill will also stop the instance your editor is actively using, so restart that connector afterwards.

Client configuration

Both clients launch the gateway over stdio. Credentials are read from this repo's .env (no secrets in the client config).

VS Code — .vscode/mcp.json (portable, already in this repo)

{
  "servers": {
    "neo4j-gateway": {
      "type": "stdio",
      "command": "uv",
      "args": ["run", "--directory", "${workspaceFolder}", "neo4j-mcp-gateway"]
    }
  }
}

Nothing is machine-specific here: ${workspaceFolder} resolves automatically. For the CodeLens Start/Restart buttons (and for ${workspaceFolder}) to work, open this repo folder as the workspace root (File → Open Folder → the neo4j-mcp-gateway folder), not a parent directory — VS Code only reads .vscode/mcp.json from the opened folder's root.

  • Start/stop it: click Start on the CodeLens above "neo4j-gateway", or Command Palette → MCP: List Servers → neo4j-gateway → Start.

  • Use it: in Copilot Chat switch to Agent mode, open the 🛠️ tools picker, and enable the neo4j-gateway tools.

  • If VS Code can't find uv: it was launched without your shell PATH. Either start VS Code from a terminal (cd neo4j-mcp-gateway && code .), install uv to a system-wide location, or replace "uv" with the absolute path from which uv.

Claude Desktop — claude_desktop_config.json

macOS: ~/Library/Application Support/Claude/claude_desktop_config.json · Windows: %APPDATA%\Claude\claude_desktop_config.json

Claude Desktop has no ${workspaceFolder} and does not inherit your shell PATH, so both paths must be absolute. Fill in your own with which uv (the uv path) and pwd (this repo's path):

{
  "mcpServers": {
    "neo4j-gateway": {
      "command": "/ABSOLUTE/PATH/TO/uv",
      "args": ["run", "--directory", "/ABSOLUTE/PATH/TO/neo4j-mcp-gateway", "neo4j-mcp-gateway"]
    }
  }
}

Tip: you can add an "env": { "NEO4J_URI": "…", "NEO4J_PASSWORD": "…" } block here instead of using .env if you prefer per-client credentials.

Sharing this repo

The repo is self-contained — a new user only needs, per machine:

git clone <repo-url> neo4j-mcp-gateway
cd neo4j-mcp-gateway
cp .env.example .env          # fill in their Neo4j URI / user / password / database
uv sync                       # creates the venv; uvx fetches the downstream on first run
code .                        # open THIS folder in VS Code, then MCP: List Servers → Start

Prerequisites: Python 3.11+, uv, and (for the Inspector smoke test) Node/npx. No absolute paths to edit for the VS Code flow; only the Claude Desktop config needs their own two paths.

Demo data (account-takeover)

bundles/ato/data/ato_demo.cypher seeds a small, self-contained ATO dataset — realistic legitimate baseline, two fraud patterns (classic takeover + mule ring), and a false-positive traveler for precision discussion. Load it with:

cypher-shell -a "$NEO4J_URI" -u "$NEO4J_USERNAME" -p "$NEO4J_PASSWORD" -d "$NEO4J_DATABASE" -f bundles/ato/data/ato_demo.cypher

It's idempotent and namespaced (source:'ato-demo'), so it won't disturb other data. See bundles/ato/data/README.md for the roster, the ground-truth scoring fields, and copy-paste detection queries.

ATO demo: bundles/ato/DEMO.md — quickest path to a working demo: load, verify, serve, and the results to expect.

Demo docs:

  • bundles/ato/data/README.md — the presenter runbook (drives the tools explicitly; good with the MCP Inspector).

  • bundles/ato/data/demo_prompts.mdconversational prompts to paste into Claude Desktop so the model orchestrates the tools itself — the assistant picks the tools and narrates the investigation.


Project layout

neo4j-mcp-gateway/
  gateway/            # ENGINE (domain-agnostic; never changes per use case)
    server.py         # entrypoint: build proxy + load bundle tools + serve stdio
    proxy.py          # spawn & re-expose the official neo4j/mcp downstream
    yaml_tools.py     # YAML discovery, validation, MCP registration, Cypher execution
    mediation.py      # entitlement mediation: prelude + filter composition
    security_tools.py # resolve-identity / secure-read-cypher for mediated bundles
    pytools.py        # load code-backed bundle tools (build_tools(ctx))
    middleware.py     # HideToolsMiddleware (hide proxied tools, e.g. read-cypher)
    bundles.py        # bundle manifest parsing + discovery
    config.py         # env + active-bundle resolution
  scripts/
    try_tool.py       # fast dev loop: run one tool, no MCP/restart
    new_bundle.py     # scaffold a new bundle from bundles/_template
    validate_bundle.py# run every tool in a bundle against a live DB
    check_entitlements.py       # conformance cases; --identity-source sweeps topology
    entitlement_surface.py      # which edges decide access; --write-guard emits DENY DDL
    ingest_business_hierarchy.py# project the HR view -> OrgUnit tree, IN_UNIT, REPORTS_TO
    ingest_coverage_teams.py    # project the coverage view -> CoverageTeam, COVERS
    load_asset_platform.sh      # the five load steps, in dependency order
  bundles/            # SWAPPABLE use cases (pick one with ACTIVE_BUNDLE)
    _template/        # skeleton copied by new_bundle.py
    ato/              # account-takeover bundle
      bundle.yaml     #   metadata + non-secret config
      tools/*.yaml    #   the 7 ATO tools
      data/           #   ato_demo.cypher + demo docs
    iam/              # investment-bank entitlements bundle
      bundle.yaml     #   security.mode: mediated + protected_labels
      tools/*.yaml    #   curated mediated tools (match/scope/return)
      data/iam_demo.cypher
    asset_platform/   # the reference entitlement model — 20 labels, 47 cases
      bundle.yaml     #   grants, denials, caller_attributes, ingested_rels
      tools/*.yaml    #   research, interactions, trade blotter, compensation
      data/views/     #   sample business_hierarchy + coverage_teams extracts
      data/*.cypher   #   business graph, authored policy, trades and compensation
  .vscode/mcp.json
  .env.example        # root creds/defaults (per-bundle .env overrides)
  pyproject.toml
  README.md

Design notes / extending

  • Namespacing — official tools keep their original names; YAML tools are prefixed (usecase_), so names can never collide.

  • Lazy driver — the YAML executor connects to Neo4j on first tool call, so the gateway starts and lists tools even if Neo4j is briefly down; connection errors surface as clean tool errors.

  • Retrieval-ready — the YAML registry (load_tool_specs in yaml_tools.py) is cleanly separated from execution, so a future vector-index / kNN routing layer could sit in front of it without touching the executor. (Not implemented — out of scope for now.)

  • Extending routing — to add non-YAML tools, register them on the gateway server in server.py with gateway.add_tool(...).

Troubleshooting

Symptom

Cause / fix

Only usecase_* tools appear

Downstream couldn't reach Neo4j and exited. Fix NEO4J_URI/creds; check gateway stderr.

uvx slow on first run

It downloads the official server wheel once, then caches it.

Claude Desktop can't start it

Use the absolute path to uv in command.

YAML tool returns an error

The message includes the Neo4j error code — verify the Cypher and params.

Available Tools

3 tools
usecase_detect_synthetic_identityA

Identifies potential synthetic identities by finding clusters of identities sharing PII elements without legitimate relationship explanation.

(Use-case tool from detect_synthetic_identity.yaml; runs curated Cypher in read mode.)

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoMaximum number of results to return
customer_idNoSpecific customer ID to investigate (omit for discovery mode)
min_shared_attributesNoMinimum number of shared attributes to flag as suspicious

TDQS

A4.2/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the burden of behavioral disclosure. It openly states that it runs curated Cypher in read mode, communicating a read-only operation and a pre-defined query. While it does not detail output format or edge cases, this is a useful behavioral trait beyond what the schema provides.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two sentences long, front-loads the core purpose, and includes a compact source/PII note. Every word adds value with no redundancy or fluff.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a three-parameter tool without an output schema, the description provides enough context for an agent to select and invoke it: it explains the purpose, the read-only mode, and the discovery-mode hint derived from the customer_id parameter. It does not describe return values, but the lack of an output schema and the straightforward nature of the tool make this acceptable.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already explains limit, customer_id, and min_shared_attributes. The tool description adds little parameter-specific meaning beyond the conceptual notion of 'clusters sharing PII elements,' which only loosely relates to min_shared_attributes. Baseline 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the specific action (identifies potential synthetic identities) and the method (clusters of identities sharing PII elements). This distinguishes it from sibling tools like movie search and high-risk transaction detection, which are entirely different use cases.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The first sentence explicitly states the tool's purpose, making it clear when to use it: when there is a need to detect synthetic identities. The note about being a use-case tool and running curated Cypher in read mode provides additional context, though it does not explicitly name alternative tools or exclusions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

usecase_high_risk_transactionsA

List a customer's outgoing transactions that benefit an account in a high-risk jurisdiction, optionally filtered to a minimum amount. Useful for AML triage.

(Use-case tool from high_risk_transactions.yaml; runs curated Cypher in read mode.)

ParametersJSON Schema
NameRequiredDescriptionDefault
min_amountNoOnly return transactions at or above this amount
customer_idYesThe Customer.customerId to investigate

TDQS

A4.2/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations are absent, so the description must disclose behavioral traits. It explicitly says it 'runs curated Cypher in read mode', indicating a safe, read-only operation with a pre-vetted query. This provides essential safety transparency, though it omits details like return schema or potential access requirements.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact, with the core action in the first sentence and a brief, useful parenthetical about the tool's origin and read-only nature in the second. No unnecessary words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The tool is simple with two parameters and no output schema. The description covers the purpose, filtering, and read-only behavior, providing sufficient context for an agent to select it. However, it does not specify the shape of the returned transactions, which might be inferred but is not explicitly stated.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema already provides full descriptions for both parameters, with customer_id and min_amount clearly documented. The description only restates the min_amount filtering capability ('optionally filtered to a minimum amount') without adding new semantic meaning, so the schema carries the weight.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's function with a specific verb ('List'), the resource ('customer's outgoing transactions'), and the unique filter ('benefit an account in a high-risk jurisdiction'). This differentiates it from sibling use-case tools like detecting synthetic identity or searching movies.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description notes it is 'Useful for AML triage', providing a clear context for when to use it. It does not explicitly mention alternatives or exclusions, but the distinct purpose and sibling names make the appropriate usage apparent.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

usecase_search_movies_by_actorA

Find movies a given actor appeared in.

(Use-case tool from example_movies.yaml; runs curated Cypher in read mode.)

ParametersJSON Schema
NameRequiredDescriptionDefault
actorYesFull name of the actor

TDQS

A3.8/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description discloses that the tool 'runs curated Cypher in read mode', which informs the agent that the operation is read-only and uses a predefined query. However, it does not elaborate on return format, permissions, or error behavior, leaving some gaps.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two sentences, with the main purpose front-loaded. The second sentence adds useful context about the tool's origin and read-only nature. No filler words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple tool with one parameter and no output schema, the description covers the essential aspects: purpose, parameter description, and read-only behavior. The lack of an output schema is acceptable given the simple purpose, though a note about the response format would improve completeness.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema already provides a full description for the 'actor' parameter ('Full name of the actor'), so the description doesn't need to add parameter details. The description implicitly references the parameter but adds no additional semantic nuance.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's function with a specific verb ('Find') and resource ('movies'), scoped by actor. It distinguishes itself from sibling tools, which target different domains (synthetic identity, high-risk transactions).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives a clear use case ('Find movies a given actor appeared in') but does not explicitly discuss alternatives or when not to use it. The sibling tools are unrelated, so no exclusions are needed, but explicit guidance is missing.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 3 tool updatesv0.1.0
    • First observedusecase_detect_synthetic_identity
    • First observedusecase_high_risk_transactions
    • First observedusecase_search_movies_by_actor

TDQS

A3.8/5.0

Scored across 3 tools

Disambiguation5/5

Each tool addresses a distinctly different use case: synthetic identity detection, movie search, and high-risk transaction analysis. There is no overlap or ambiguity in their intended purposes.

Naming Consistency3/5

All tools share the consistent 'usecase_' prefix, but the action part varies: 'detect', 'search', and a noun phrase 'high_risk_transactions' (no verb). This mix of verb_noun and noun-only patterns is somewhat inconsistent.

Tool Count3/5

With only 3 tools, the set is on the low side for a Neo4j gateway that could plausibly support many graph operations. It feels like a small demo set rather than a comprehensive service, but it is not absurdly thin.

Completeness2/5

The tools are a random assortment of curated use cases with no clear domain coverage. There is no general query, write, or management capability, and the movie search seems unrelated to fraud/AML use cases, leaving significant gaps for any given purpose.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    A
    maintenance
    Local-first MCP proxy with BM25 tool discovery, quarantine security, Docker isolation, OAuth support, activity logging, and web UI. Routes multiple upstream MCP servers through a single endpoint.
    9
    377
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    A lightweight MCP gateway that aggregates multiple MCP services into a unified stdio interface, automatically prefixing tool names with the service name to avoid conflicts.
    14 npm
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    A zero-dependency MCP gateway: host your own tools, forward and curate tools from other MCP servers, expose them leanly to cut agent token cost, and gate every call through your own policy hooks before it runs.
    4
    13 npm
    37
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    An MCP gateway and offline catalog CLI that aggregates multiple upstream MCP servers into a single stdio endpoint, and provides tool recommendation and catalog inspection without connecting to upstreams.
    MIT