Skip to main content

mcpindex-gate

The in-path drift gate for agent tool calls - the open-core client of mcpindex.ai, the drift-monitored MCP server index. It runs on your host, sends no credentials anywhere (wiring does pass your server env through its process), and checks every MCP tool definition against your own pinned baseline before a call goes out. The Python SDK wrap() path posts a credential-blind receipt per gated call to mcpindex.ai by default; set MCPINDEX_RECEIPT_INGEST_ENABLED=0 for zero egress. The mcpindex-config-wire install wires the stdio proxy, and the proxy does not emit receipts today, so that path sends nothing. See "Call receipts" below.

Renamed: previously published as mcpindex-preflight. That name remains a deprecated alias that depends on this package. New installs should use mcpindex-gate.

What it does

mcpindex-gate produces a drift decision - PROCEED / HOLD / INCONCLUSIVE - by diffing the live tool contract against the contract you pinned. If a server silently changes a tool's schema, description, or surface, the gate can HOLD the call and surface why.

from mcpindex_gate import wrap, PreflightPin, PreflightHold

session = wrap(your_mcp_client_session, pin=PreflightPin(path="~/.mcpindex/pin.json"))

try:
    result = await session.call_tool("transfer_funds", {...})
except PreflightHold as hold:
    # The tool contract drifted from your pin - inspect `hold` and decide.
    print(hold)

wrap() accepts any duck-typed client session; this package does not depend on the mcp SDK.

What it is - and is not

  • It is a contract diff, not a safety oracle. It detects that a tool changed; it does not judge whether the change is malicious.
  • A HOLD means "this drifted from your pin - look before you act." It is advisory. It does not block attacks, guarantee safety, or make a server tamper-proof.
  • It mints no clearance verdict. The offline client can detect drift and HOLD; it can never publish a "SAFE" verdict.

What status tells you (0.11.0)

mcpindex-config-wire status reports enforcement, not merely that our proxy is in the launch line:

  • [x] requires that nothing is un-wired, unreadable, or non-enforcing. Before 0.11.0 the mark appeared as soon as one server was wired and could never turn off.
  • [!] means real coverage gaps, and names them by host.
  • notify-only counts servers launched with --posture monitor, which releases every drift hold (a tamper hold and an internal error still stop the call). It is a legitimate choice, but it is not protection, so it is never counted as such.
  • will not start (bad gate command) (0.13.2) counts servers whose launch line the proxy rejects outright, so the server never starts and you lose the server as well as the protection. Nothing we wire produces this; it comes from a hand edit to the config. Fix it by un-wiring that host and wiring it again, which rebuilds the launch line from scratch.
  • keeps its baseline outside mcpindex (0.13.4) counts servers whose config entry points --pin-store at a file outside ~/.mcpindex/pins/. That server may well still hold, but against a baseline nothing else here can read, and one whose deletion the gate can no longer detect, because the record of what was pinned lives beside the store and moves with it. Wiring never writes that flag, so an entry only carries one because something edited your config. It is not counted as protection, and mcpindex-config-wire wire --repin moves the baseline back. In 0.13.3 and earlier these servers were counted as fully enforcing.
  • hosts scanned: N is stated because project-scoped configs (a repo's own .mcp.json, .cursor/mcp.json, .vscode/mcp.json, ...) are not scanned yet. The total is a floor, not a census.

If the gate HOLDs on your baseline

A HOLD whose reason names the baseline rather than a tool contract means the pin store for that server was present but unreadable, or was deleted after having been pinned. The gate refuses to proceed rather than silently re-learning the contract from whatever the server is serving right now. This holds in every posture and regardless of fail_open, because neither is consent to trust a baseline that may have been rewritten.

To recover deliberately, run mcpindex-config-wire reset-pins <server> (add --config PATH when the entry is not in a default host config, and --yes to skip the confirm). That removes the pin store and its ever-pinned record for that server. The next list pins from whatever the server serves at that moment - a reset re-trusts the current contract - so do it only when you know why the store was unreadable (an interrupted write, a restored backup, a machine migration).

Honest limit: this is tamper-evident, never tamper-proof. A process running as you that replaces the store with a well-formed one carrying its own baseline is still trusted. No same-user control prevents that: any secret the gate can read, a process running as you can read too.

Install

uv tool install mcpindex-gate

One-click host wiring + a resident auto-onboard watcher are available via the installer at https://mcpindex.ai/install.sh.

Call receipts (on by default, SDK wrapper only)

Receipts are emitted by the Python SDK wrap() path. The mcpindex-config-wire install wires the stdio proxy, and the proxy does not emit receipts today, so a wire install sends nothing. Naming which half sends is the point.

After each gated call the gate emits a compact, credential-blind receipt: a hash of the tool identity, the gate verdict, and the action class (read / write / execute). It never includes tool arguments, result content, server names, or URLs. Receipts are linked by a random per-install token stored in ~/.mcpindex/install_id -- pseudonymous, not derived from you or your machine. Keyless installs have no account link; if you sign in on mcpindex.ai and configure an api_key, receipts from that install are associated with your key.

From gate v0.9.0, the first run on a machine prints a one-line disclosure of exactly this behavior to stderr and sends nothing that session -- emission starts on run 2, so no receipt ever leaves before the notice and the opt-out were visible. Earlier versions emit without the runtime notice, per this README.

After 10 gated calls the ambient notifier fires a one-line message on stderr:

mcpindex - 10 tool calls noted - mcpindex.ai/receipts?id=<your-install-id>

That link shows your per-install call log. To suppress receipt egress entirely:

export MCPINDEX_RECEIPT_INGEST_ENABLED=0

Weekly summary line (local, zero egress)

At most once per ISO week the gate prints one line summarizing what it did last week -- calls gated, tools and servers watched, drift seen, holds issued:

mcpindex - week 2026-W29: 240 call(s) gated across 12 tool(s) / 3 server(s), 0 drift - your baseline is holding - mcpindex.ai/receipts?id=<your-install-id>

The counters live only in ~/.mcpindex/weekly_stats.json; nothing about this line is sent anywhere. To silence it:

export MCPINDEX_WEEKLY_SUMMARY=0

Project config scanning (local read, ON by default)

mcpindex-config-wire counts the MCP servers you actually run. Many of them are not in your global host config: they are declared per-project, in a repo's own .mcp.json, .cursor/mcp.json, .vscode/mcp.json, .gemini/settings.json or .zed/settings.json. A gate that cannot see them reports a coverage number that is a floor, not a census - on one real machine it said 25 servers while 35 were loaded.

To find those repos it reads the project list Claude Code already keeps in ~/.claude.json. This widens what the tool reads from about ten fixed paths in your home directory to every repository you have opened in Claude Code. It is a local, read-only scan and nothing about it is sent anywhere, which is stricter than the gated-call path: that one posts a receipt by default.

Detect-only. Project configs are counted and reported, never wired. They are version-controlled and shared, so wiring one would commit a change pointing your teammates at a gate they have not installed - and every server would fail for them. Wiring one is possible, but it takes an explicit --config <path>, after you have read the file.

To turn the scan off, or to add a repo Claude Code does not know about:

mcpindex-config-wire status --no-project-scan      # or: export MCPINDEX_PROJECT_SCAN=off
mcpindex-config-wire status --project ~/work/my-repo   # repeatable; for Cursor / Gemini / Zed users

With the scan off, the server count is a floor and status says so rather than printing a smaller number without the caveat.

Drift telemetry (opt-in, OFF by default)

The gate can report that a tool's contract drifted - so mcpindex can track drift on servers it can't crawl itself (private / auth-gated). It is off by default and sends nothing unless you turn it on:

export MCPINDEX_DRIFT_TELEMETRY=detection   # off (default) | lookup | detection | contribute
  • off - the default. Sends nothing, asks nothing.
  • lookup - read-only. Asks whether a tool's contract has already drifted, so the gate can warn you on the first call. Sends one fingerprint per tool, which is a key over a public registry rather than anonymity, and reports none of your own catches back.
  • detection - lookup, plus each tool you pin and each contract drift sends one one-way signal.
  • contribute - reserved for a future richer tier; behaves identically to detection today.

Each tier is a superset of the one before it. See https://mcpindex.ai/privacy and https://mcpindex.ai/docs for exactly which fields leave the machine.

Where the setting lives (v0.13.0+)

For servers you wired through the gate, set it with the wiring command rather than in your shell - the wired entry is what the proxy actually launches with:

mcpindex-config-wire --drift-telemetry lookup    # or detection / contribute / off

Your choice is recorded in ~/.mcpindex/consent.json and re-applied on every later re-wire, including on servers already wired.

0.13.2 and earlier: this did not work. Wiring skipped servers it had already wired, so on an already-configured machine --drift-telemetry off reported success and cleared nothing. On those versions you must pass --repin. Fixed in 0.13.3.

Before v0.13.0 the setting lived only in the host config entry, where the gate could not tell your own opt-in from one something else had written - so wiring cleared it, and a scripted re-wire (--yes, an editor auto-wire) silently turned your setting off.

The record holds the mode, a label for how it was set, when, and - when the setting was carried over from an existing wired config - the host label it came from. No paths, no server or tool names. (In 0.13.1 and 0.13.2 that field held a file path from your machine instead.) It is local, is never sent anywhere, and is not a security boundary: anything running as your user can edit it, exactly as it can edit your host config. What it buys is that your setting survives, and that a wired entry which disagrees with it gets reported instead of silently obeyed.

To turn everything off and clear it from every wired entry:

mcpindex-config-wire wire --drift-telemetry off

Upgrade, pin-versions, and review (0.18.0 and later)

Under guard and strict, a new tool that appears on a server you already pinned is left out of the tool list and a call to it holds. It is not trusted mid-session on first sight. A process running as you that rewrites the pin store with its own well-formed pins is still trusted, as the honest-limit paragraph above says. Pass --new-tools tofu (or new_tools=tofu on the wrapper) only when you want that older first-sight behaviour back for that server.

The pin records every top-level field a server lists for a tool: title, icons, _meta, execution, and any field a later spec adds, plus annotation keys other than the four hint booleans. A change to any of them on a pinned tool is drift: under guard and strict the tool is left out of the list and its call holds; monitor shows it. A pin written before 0.19.0 records those fields once, from the first list whose definition matches, and compares them from then on. A field that changes between the upgrade and that first list is trusted the way a first-seen tool is. A first outputSchema is held the same way when it carries any string or any property name, which covers most structured output: expect one review per tool when a server starts returning it. A bare {"type": "object"} is shown.

The proxy and the Python wrapper (on_context_drift=) read the server's instructions, pin them beside the tool pins and report a change. They never edit or withhold the text.

Upgrade a wired server

mcpindex-config-wire upgrade demo --to 1.1.0

Example (from the upgrade approval fixtures):

demo: launch npx -y pkg@1.1.0
demo: new=1 changed=0 removed=0 unchanged=0
{
  "description": "Echo a line",
  "inputSchema": {
    "properties": {},
    "type": "object"
  },
  "name": "echo"
}
demo: pinned

The command rewrites the wired launch to the target version, shows the review, and pins what the probe listed after you confirm. First-recorded fields and real changes both need a confirm when stdin is a TTY (or pass --yes).

Record launch versions on existing pins

mcpindex-config-wire pin-versions

Example:

filesystem/read_file: title = "Read File (Deprecated)"
filesystem: stamped=1 first_recorded=1 changed=0 new=0 removed=0
context7/resolve-library-id: outputSchema = {"properties": {"id": {"type": "string"}}, "type": "object"}
context7/resolve-library-id: title = "Resolve Library Id"
context7: stamped=1 first_recorded=1 changed=0 new=0 removed=0

pin-versions writes the launched version onto pins whose definition still matches, and asks once before recording first-recorded fields (titles and other fields the pin never stored). A real field change uses the upgrade confirm path.

List and approve held tools

mcpindex-report pending
mcpindex-report approve demo note sha256:...
mcpindex-report approve --all

Example pending output:

srv note changed-without-upgrade sha256:...
{
  "description": "Write a note for the operator",
  "inputSchema": {
    "properties": {},
    "type": "object"
  },
  "name": "note"
}

approve <server> <tool> <hash> pins one held definition when the digest matches. approve --all lists each floating row, asks for confirm, then pins only those digests. It refuses changed-without-upgrade records; those need an explicit approve or an upgrade after you have reviewed the change.

Metadata

Release files for mcpindex-gate 0.21.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distribution (wheel)

Table of built distributions (wheels) for mcpindex-gate 0.21.0
File Interpreter ABI Platform
mcpindex_gate-0.21.0-py3-none-any.whl Python 3 none any Details

Release files / mcpindex_gate-0.21.0-py3-none-any.whl

Download URL mcpindex_gate-0.21.0-py3-none-any.whl
Size 539.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6c3411d9a223f973b6ce282e2df1251262a0cfb876c8cebd5e6e0721356b0acc
BLAKE2b-256 checksum
How to use checksums
d5fab9ee460e210c133c2e7dac54ce373903c9d220c8b670fa363e452daea3cd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.21.0 This release

1 release file

0.20.0

1 release file

0.19.0

1 release file

0.18.0

1 release file

0.17.0

1 release file

0.16.2

1 release file

0.16.1

1 release file

0.16.0

1 release file

0.15.0

1 release file

0.14.0

1 release file

0.13.5

1 release file

0.13.4

1 release file

0.13.3

1 release file

0.13.2

1 release file

0.13.1

1 release file

0.13.0

1 release file

0.12.2

1 release file

0.12.1

1 release file

0.12.0

1 release file

0.11.2

1 release file

0.11.1

1 release file

0.11.0

1 release file

0.10.0

1 release file

0.9.2

1 release file

0.9.1

1 release file

0.9.0

1 release file

0.8.0

1 release file

0.7.0

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page