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.

F
license - not found
A
quality
B
maintenance

Maintenance

0Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

  • Governed MCP gateway: one endpoint for your tools, with credential custody and audit log.

  • Unified gateway exposing 150+ tools across all NexGenData MCP servers via one endpoint.

  • nexusOAuth

    Zero-setup MCP gateway securely connecting AI to your tools with authentication and workflows

  • Self-hosted MCP gateway: turn any API, database or MCP server into AI connectors — no code.

View all 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
    326
    MIT
  • A
    license
    Not graded
    quality
    B
    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.
    18
    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.
    2
    4
    25
    35
    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

View all related MCP servers

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jeffneo/neo4j-mcp-gateway'

If you have feedback or need assistance with the MCP directory API, please join our Discord server