Skip to main content

Security Quarantine

MCPProxy includes a quarantine and review system that reduces exposure to Tool Poisoning Attacks (TPA). It withholds unapproved servers from agents until a person releases them, and it holds new or changed tool definitions for review (under manual trust). Tool-level approval is not human-only: an MCP client with admin access can approve held tools itself (see below). It is an admission and review control, not a guarantee that an approved tool is safe; see What quarantine does and does not cover.

What is a Tool Poisoning Attack?​

Tool Poisoning Attacks occur when malicious MCP servers:

  1. Hidden Instructions: Embed malicious instructions in tool descriptions that AI agents might follow
  2. Data Exfiltration: Trick AI agents into sending sensitive data to external servers
  3. Credential Theft: Attempt to extract API keys or tokens
  4. System Manipulation: Try to execute unauthorized commands

How Quarantine Works​

Automatic Quarantine​

When a new server is added via an AI client (using the upstream_servers tool):

  1. Server is automatically placed in quarantine status
  2. Tool calls to quarantined servers return a security analysis instead of executing
  3. Server remains quarantined until manually approved

Servers added by hand-editing mcp_config.json​

Since issue #937, the same admission gate applies to servers written directly into the configuration file — hand-editing is a normal workflow, and config files get shared, templated and copied between machines, so an entry from a config edit is admitted on exactly the same terms as one from mcpproxy upstream add.

A config-file server is held for review when both of the following are true:

  • the entry does not contain a quarantined key (writing "quarantined": false is an explicit operator statement and is obeyed), and
  • the server is not yet recorded in config.db.

Quarantine must also be enabled (quarantine_enabled, on by default), and the server's trust mode must not be auto. A server with no trust_mode is manual unless a legacy setting resolves to auto ("auto_approve_tool_changes": true, or the older "skip_quarantine": true), so by default a first-seen config-file server is held; a server whose trust mode is auto is admitted.

The second condition is what makes upgrading safe: every server you are already running has a config.db record, so upgrading never re-quarantines a server you have already vetted. "Vetted" means the record is backed by an approval baseline — at least one of the server's tools was approved at some point, which happens automatically when a trusted server first connects. A server recorded as unquarantined whose tools were never approved was never admitted, and the gate holds it for review (see the restart note below).

Because the rule cannot tell such a server apart from a vetted one that has no tool records, a few vetted servers are also held once on upgrade: servers that expose no tools (prompts or resources only), servers that never connected while live (for example, disabled ever since they were vetted), and servers last vetted before v0.21. Approve them from the quarantine review; the decision is recorded and they are not held again. To skip the review, add "quarantined": false to the server's entry before upgrading. The boundary is that a server present in a hand-written config but absent from config.db — after a wiped data directory, or on a machine that has never seen that config before — is treated as first-seen and held for review.

The decision is durable: once recorded, a quarantine is not reverted by a config file that never mentions the key. Un-quarantining stays a user action, and writes both the config file and config.db.

No TPA scan runs at config-load admission; quarantine-by-default already keeps the tools away from agents, and the scan is what the human review step performs.

If mcpproxy writes the configuration file itself (any API apply, a quarantine toggle, an upstream add), it does not stamp "quarantined": false onto servers that never stated it — a value mcpproxy invented would otherwise read back as an operator statement and disable the gate for that server.

Upgrading from a release affected by #937 — action required​

The gate treats "present in config.db" as "has already been through admission". That is what keeps upgrades safe, but it also means an install that was already hit by #937 is not remediated by upgrading: the buggy admission left exactly the config.db record the gate now reads as vetted, so a server that was admitted unquarantined stays live.

At startup mcpproxy logs a warning naming any server that looks like this — configured, running unquarantined, never explicitly reviewed, and with a trust_mode that would have held it:

Configured servers predate the config-load admission gate and have never been
explicitly reviewed servers=["suspicious-server"]

For each server named, either:

  • review it and record the decision — quarantining and then releasing it from the quarantine UI writes an explicit "quarantined" value to mcp_config.json, which silences the warning; or
  • write the decision by hand: add "quarantined": true (hold it) or "quarantined": false (you have vetted it) to that server's entry.

Adding the key by hand is enough — the gate obeys an explicit value either way.

Restarts also cleared the quarantine. In the same affected releases, restarting a server that the gate had quarantined silently cleared its quarantine: a restart from the REST API, the CLI, the tray or an MCP client, "restart all", a secret change that restarts the servers using it, and a security scan (including the baseline scan that runs shortly after startup) all re-read the server from mcp_config.json and wrote the un-gated entry over the recorded quarantine. Upgrading now catches this case: such a server has no approved tool, so the gate quarantines it again at startup and logs Quarantining known server with no approved tool baseline; approve it from the quarantine review as usual. A server that ran unquarantined (and so has approved tools) still only shows up in the "predate the config-load admission gate" warning. From this fix on, a restart runs the file entry through the admission gate, and config.db refuses to lower a recorded quarantine unless the operator states "quarantined": false or the server is released from the quarantine review.

Tool Discovery and Search Isolation​

Quarantined servers are completely isolated from the tool discovery and search system:

FeatureQuarantined ServerApproved Server
Tools indexed❌ No✅ Yes
Tools searchable via retrieve_tools❌ No✅ Yes
Tools appear in HTTP API search❌ No✅ Yes
Tool calls allowed❌ No (returns security analysis)✅ Yes

This isolation prevents Tool Poisoning Attacks from:

  • Injecting malicious descriptions into search results that AI agents might read and follow
  • Appearing in tool recommendations where they could be mistakenly selected
  • Influencing AI agent behavior through carefully crafted tool metadata

When a server is quarantined:

  1. Its tools are immediately removed from the search index
  2. retrieve_tools queries will never return tools from that server
  3. The server remains visible in the server list (marked as quarantined) for management

When a server is unquarantined (approved):

  1. The server connects to discover its tools
  2. Tools are indexed and become searchable
  3. Tool calls are allowed to execute normally
  4. Any pending (newly-discovered, never-reviewed) tool-approval records for the server are auto-promoted to approved — approving a server means you trust its current tool snapshot (baseline trust). Tools whose description or schema later changes (changed, i.e. rug-pull) are not affected and stay blocked until you re-approve them explicitly.

Security Analysis​

When a tool from a quarantined server is called, MCPProxy blocks the call and returns a structured security response instead of invoking it — so the tool's description can be reviewed before it ever runs:

{
"status": "QUARANTINED_SERVER_BLOCKED",
"serverName": "suspicious-server",
"toolName": "fetch_data",
"message": "🔒 SECURITY BLOCK: Server 'suspicious-server' is currently in quarantine for security review. Tool calls are blocked to prevent potential Tool Poisoning Attacks (TPAs).",
"instructions": "To use tools from this server, please: 1) Review the server and its tools for malicious content, 2) Use the 'upstream_servers' tool with operation 'list_quarantined' to inspect tools, 3) remove from quarantine if verified safe",
"toolAnalysis": {
"name": "fetch_data",
"description": "…",
"inputSchema": { "…": "…" },
"serverName": "suspicious-server",
"analysis": "SECURITY ANALYSIS: This tool is from a quarantined server. Please carefully review the description and input schema for potential hidden instructions, embedded prompts, or suspicious behavior patterns."
}
}

The actual pattern detection — hidden-Unicode smuggling, cross-server shadowing, decoded shell payloads, injection/exfiltration phrases, and embedded secrets — is performed by the deterministic offline detect engine that backs the built-in tpa-descriptions scanner. Its findings appear in the scan report (mcpproxy security report <server>), each carrying a rule_id, severity, threat_level, confidence, and the contributing check signals. See Tool Scanner for the full rule reference.

Prompt rug-pull baseline (spec 100)​

When upstream prompt aggregation is enabled (aggregate_upstream_prompts: true, off by default), mcpproxy keeps a per-prompt approval baseline that mirrors the tool rug-pull machinery. A trusted server that ships a benign prompt and later mutates its advertised metadata (name, description, or argument descriptions) has that changed prompt withheld from prompts/list until it is approved — closing the gap where a subtler injection that passes the poison scanner could still slip in via a later edit.

  • Scope — metadata only. The baseline hashes the prompt's advertised metadata, not its prompts/get message content (which is materialised fresh per call and has no list-time artifact to baseline). Content is defended separately by output sanitisation and size caps. This is an inherent limit, not a shortcut.
  • Enforcement is by withholding. A held prompt is simply not registered, so it is absent from prompts/list and prompts/get on it fails natively. There is no separate runtime gate.
  • Trust parity. A server with trust_mode: auto (or auto_approve_tool_changes: true) auto-approves its prompt changes; manual holds them. Disabling quarantine globally (quarantine_enabled: false) auto-approves.

Manage held prompts with the quarantine_security MCP tool:

// see what is held (all servers, or one via "name")
{ "operation": "inspect_prompts", "name": "github" }
// approve one held prompt, or all for a server
{ "operation": "approve_prompt", "name": "github", "prompt_name": "summarize_pr" }
{ "operation": "approve_all_prompts", "name": "github" }

What quarantine does and does not cover​

What it does, with quarantine enabled (the default):

  • Withholds unapproved servers and tools. A quarantined server's tools are not indexed, not returned by retrieve_tools, and calls to them are refused (the refusal carries a security analysis of the tool). A new or changed tool on a trusted server is held separately until approved.

  • Holds changed definitions for review. Each tool's name, description, input schema, output schema (when declared) and safety-relevant annotation hints are hashed. A different hash after approval is held as "changed" instead of being silently trusted (see Tool-Level Quarantine), in every trust mode except auto. Only serialization noise is absorbed without a hold: an input schema that differs from the approved one only in key order or the order of its required list (same properties, types, constraints and required set) is re-baselined, while any structural input-schema difference (an added, removed or retyped parameter) is held. An output schema or description added to a tool approved without one is held as a change too: the empty stored value is the declared contract, not a wildcard. Two upgrade wildcards remain, both for approval records written by older releases. A record written before complete contract capture treats an empty stored field as "never captured", and the live value stands in once during the upgrade migration. Separately, any record approved before annotation hints joined the hash (hash schema below v4, including records that did capture the full description and schemas) and that never stored an annotation snapshot has unknown hints: the first discovery under the new release accepts and baselines the live hints instead of holding them, so a hint flip that lands before that first pass is not held. After that pass, hint flips are held like any other change. In scan mode (the default) a deterministic structural gate runs first and then the offline scan of the changed contract decides: a clean, full-coverage verdict with no structural signal approves the new contract in the same discovery pass (reason scan_approved, activity event tool_auto_approved carrying the replaced contract); a finding, a scan that could not cover the tool, or any structural signal (see Structural changes are held) holds it with the evidence attached. In manual mode a changed contract is always held: the scan runs and its findings are recorded as hold evidence, but a clean verdict does not approve the change.

    Since hash schema v5 every hashed field is length-prefixed and a missing output schema is encoded explicitly. Under v4, fields were joined with |, so moving text between the description and a schema could produce another contract with the same hash. On upgrade, a v4 approval is re-hashed without review only when its stored v4 hash covers the stored definition and that definition is exactly what the server serves now. Any other definition is held as "changed". mcpproxy tools preflight --pin values (sha256/v<N>:…) carry the schema version, so pins taken before the upgrade report hash_mismatch until re-pinned.

  • Keeps server release in human hands. There is no server-level approve over MCP, so an agent cannot release a quarantined server. This applies to server admission only. Tool approval is different: an MCP client with admin access (for example the main /mcp connection with the API key, not a scoped agent token) can call quarantine_security with approve_tool or approve_all_tools and release a pending or changed tool on an already-admitted server, with no human confirmation step. Agent tokens are refused ("Agent tokens cannot perform quarantine security operations"). Treat any admin-level MCP client as able to approve tool changes.

What it does not do:

  • It is not proof of safety. Review is only as good as the reviewer, and the offline scanners can miss attacks. A clean scan means the checks found nothing in the captured definitions; it is not evidence that a server or tool is safe. Manual review, scanner-assisted review and agent-assisted inspection (inspect_quarantined) are inputs to your decision. None of them approves anything by itself.
  • It does not see behavior outside the definition. The hash covers name, description, schemas and the safety-relevant annotation hints. A server whose implementation or backend changes while its definitions stay identical is not detected, and neither is a malicious tool result returned at call time.
  • Trust modes change the guarantee. scan (the default since spec 119) admits a new server, a new tool and a changed tool contract automatically when the offline scan is clean, so a detector miss — including a paraphrased rug pull that scans clean — is admitted at that point; findings, uncovered tools and every structural change (a safety-hint change in either direction, a widened input schema, an output-schema change, a dispatch-tier change) are held. manual holds every new server and every tool contract change until it is approved: server release needs a person, while tool approval can also come from an admin-access MCP client. auto admits without scanning or holding at all and trusts tool changes (rug-pull risk). Server admission and tool-change review are separate controls: "quarantined": false on a server entry only releases the server-level quarantine, and under manual trust a new or changed tool on that server is still held after its baseline. Tool-change holds are removed by trust_mode: auto, the legacy auto_approve_tool_changes: true (which an explicit trust_mode overrides), or quarantine_enabled: false globally. See Trust modes.
  • It is not data-loss prevention. See Sensitive Data Detection and Output Sanitisation for what is flagged and what, if anything, is blocked.

Managing Quarantine​

Review verbs and where to find them​

Every surface offers the same four decisions. Only the scan-gated approval can release a quarantined server; none of the first-party surfaces uses the legacy POST /unquarantine endpoint.

VerbREST
Approve serverPOST /api/v1/servers/{id}/security/approve. Scan-gated; force only after the dangerous-verdict confirmation; optional block: [tools] keeps tools disabled
Reject serverPOST /api/v1/servers/{id}/security/reject
Approve toolPOST /api/v1/servers/{id}/tools/approve
Reject toolPOST /api/v1/servers/{id}/tools/block
SurfaceWhere
Web UIThe Review queue (/review) and one server's review screen (/review/<server>, also the Review tab of the server detail page). The Tools page links each new or changed tool to it, and its "Needs review" count opens the queue
macOS appSidebar Review Queue and its review sheet. The tray item "Review Queue… (N)" and the Needs Attention rows open the sheet
CLImcpproxy review list, show, approve, reject (Review Commands)
Tray on Windows and Linux (Go tray)A server in the "Security Quarantine" submenu opens the Web UI at /review/<name>
MCPquarantine_security with list_quarantined, inspect_quarantined, inspect_tools, approve_tool, approve_all_tools, block_tool, block_all_tools (admin only). There is no server-level approve over MCP by design: an agent cannot release a quarantined server

Scan coverage on the review screen​

The scan line of the review screen says whether the baseline scan describes the definitions you are looking at:

CoverageWhat the screen shows
currentBaseline scan: clean · risk 0/100 · covers all 5 tools, in the colour of the verdict. The risk score appears only here
staleA warning: a tool definition changed or was added after the last scan. It names the tools and the last result, and offers Rescan. A rug pull after the scan therefore never reads as clean
not capturedA warning that the scan was not checked against tool definitions, with Fetch tool definitions (CLI: mcpproxy review fetch <server>)
no tools scannedA warning that the last scan did not analyse tool definitions, with Rescan
scanningScan in progress…
noneNot scanned yet. with Scan now

Each tool's scan verdict follows the same rule: clean only when the scan covered that tool's current definition. After a baseline scan has listed a quarantined server's tools, MCPProxy captures the definitions itself, so the review list is not empty until someone clicks Fetch tool definitions or runs mcpproxy review fetch <server>. With security.auto_baseline_scan: false and no manual scan nothing is started automatically.

Coverage is bound to the definition itself, not only to timing. When a scan exports a tool's definition it records a SHA-256 digest of the description and input schema (tool_hashes in the scan context). A tool counts as covered only if that digest equals the digest of its current definition, so a benign definition that was scanned and then swapped for a poisoned one inside the same timing window reads as stale. Scans stored before this field existed carry no digests and keep the earlier name-and-timing rules. Approval still re-checks the definition independently; this only keeps the review display honest.

On a server that is not quarantined, the review tab shows approved state: approved tools read Approved or Blocked (no Approve or Reject), the heading says the server is approved, and Manage tools and Quarantine to review again… are offered. The second is the existing quarantine action behind a confirmation. Only a new or changed tool shows Approve and Reject.

Scan a Server for TPAs (MCP)​

The quarantine_security tool can also run and read the TPA scan, so an agent reviewing a held server does not have to leave for the CLI or web UI:

// run the offline baseline scan (in-process, no Docker required)
{ "operation": "scan_server", "name": "github" }
// read the latest verdict + findings
{ "operation": "get_scan_report", "name": "github" }

scan_server answers with the verdict when the scan settles quickly, otherwise with the job id and "status": "scan started" — poll get_scan_report for the result. Every list_quarantined, inspect_quarantined and inspect_tools response also carries a one-line scan_status, so a server nobody ever scanned reads as never scanned — run scan_server first instead of looking clean.

The optional Docker-based deep scanners are a separate layer: they run only when deep scan is enabled, and when they are unavailable they are skipped without changing the baseline verdict.

View Quarantined Servers​

Web UI:

  1. Open Review queue in the Protect section.
  2. Select the server that is waiting for review.
  3. Read the server-provided tool definitions, scan result, tier counts, and changed-definition diff before making a decision. Definitions are displayed as plain text because their content is not trusted.

The queue also retains recent scan history. Old /security links redirect to the queue; a specific report remains available at /security/scans/:jobId. Optional scanner setup lives at Settings → Security → Scanners.

CLI:

mcpproxy review list
# One row per server that needs review: quarantined servers and trusted
# servers with new or changed tools

mcpproxy review show github [--full]
# Captured tool definitions, tiers and scan verdicts for one server

Approve a Server​

Web UI:

  1. Select the quarantined server from Review queue.
  2. Choose the tools to allow. Only read-only tools whose scan is clean start checked; write, destructive, unannotated, not-scanned and held tools start unchecked. Unselected tools are submitted as explicit blocks with the approval decision and stay blocked until you enable them on the Tools tab.
  3. Choose Approve server; the button names the exact count (for example "Approve server (3 of 9 tools)"). Approve all is a separate action that allows every pending or changed tool (tools you blocked earlier on a re-quarantined server stay blocked). If no tool definitions have been captured, the button reads "Approve without seeing tools" and the UI asks for a separate confirmation before a blind approval can proceed. A blind or empty-review approval still records the server's tool baseline (see Durable tool baseline), so any tool the server lists later waits for review instead of being trusted automatically.

The approval is bound to the review on screen. The Web UI, the CLI and the macOS tray send the review fingerprint of every tool they showed. If a tool's definition or safety hints changed, or a new tool appeared, after the review was loaded, nothing is approved: the server stays quarantined, the review reloads and shows "The review is out of date ...". Approve again after checking the reloaded list. A server approval promotes only the tools that were pending when it was committed; a tool the server lists afterwards waits for review. See Review-bound approvals.

The review controls are deliberate: Fetch tool definitions (CLI: mcpproxy review fetch) uses the inspection-only discover-tools capture to store current upstream metadata without indexing it, Allow this tool selects an individual tool for the server decision, Approve server releases only the selected tools, and Reject server keeps the server quarantined. Existing trusted servers use the same screen to approve or reject a changed tool definition.

API:

curl -X POST \
-H "X-API-Key: your-key" \
http://127.0.0.1:8080/api/v1/servers/server-name/discover-tools

# The capture action reports success only. Read the stored, redacted review
# payload before deciding which tools to block.
curl -H "X-API-Key: your-key" \
http://127.0.0.1:8080/api/v1/servers/server-name/review

# Submit the scan-gated approval. Omit block[] or list only the tools to keep
# disabled; direct unquarantine is legacy-only. expected_hashes binds the
# approval to the review you read (each tool's current_hash; {} for an empty
# review): a tool that changed since answers 409 review_out_of_date.
curl -X POST -H "X-API-Key: your-key" -H "Content-Type: application/json" \
-d '{"block":["tool-to-keep-disabled"],"expected_hashes":{"read_note":"rv2-...","tool-to-keep-disabled":"rv2-..."}}' \
http://127.0.0.1:8080/api/v1/servers/server-name/security/approve

CLI:

# Quarantined server: scan-gated approval. By default only read-only tools with
# a clean scan are allowed; --all allows every tool, --tools a,b exactly those,
# --except keeps more tools disabled
mcpproxy review approve github [--all | --tools a,b] [--except a,b] [--force] [--yes]

# Trusted server: approve only the listed new or changed tools
mcpproxy review approve github --tools create_issue

# Bind the approval to a review you saved and read first
mcpproxy review show github -o json > review.json
mcpproxy review approve github --from-review review.json --yes

See Review Commands for every flag.

Configuration:

Leave a new server quarantined until the review flow has captured its tool definitions and the scan-gated approval is complete. Do not edit quarantined: false into the configuration file as an approval shortcut: that bypasses the informed-review record and its baseline check.

Removing a server purges its security state​

Removing a server (REST DELETE /api/v1/servers/{id}, MCP upstream_servers remove, the Web UI, or a config edit that drops it) deletes everything MCPProxy stored under its name in the same commit, before the call returns: approved tool and prompt hashes, OAuth tokens and the DCR client registration, scan reports and jobs, the integrity baseline, call history and tool hashes, plus its search-index entries and refresh schedule. OAuth state is matched exactly, so removing foo never signs out foo_bar.

A server added again under the same name is therefore a new server that inherits none of the old one's records: an unforced scanner approval needs a new scan, and it needs a new OAuth sign-in. Admission policy applies to it as to any new server. Under the default manual trust it starts quarantined and every tool and prompt it exposes is pending review, even when the definitions are identical. With trust_mode: "auto" or an explicit quarantined: false it is admitted per that setting, and tool discovery can record a fresh approved baseline for it. Removal purges the inherited records; it does not override the admission policy.

OAuth credential writes are bound to the server's incarnation too (Spec 116 PR2b). If the removed server's connection was saving a token, completing a login or registering a DCR client at the moment of removal, that write is discarded when it lands: nothing is written for the name afterwards, and a server re-added under the same name starts signed out. The same holds when a server's OAuth configuration changes: credentials of the old configuration are cleared and never come back.

An approval made on the old server cannot land on the new one. Each server has an incarnation (in GET /api/v1/servers and the review payload) that changes only on remove and re-create; approvals are bound to it, and the REST approval routes accept it as expected_incarnation. A decision bound to a removed incarnation writes nothing and answers 409 server_replaced. See REST API: Removal and incarnation.

Re-quarantine a Server​

If you need to quarantine a previously approved server:

curl -X POST \
-H "X-API-Key: your-key" \
http://127.0.0.1:8080/api/v1/servers/server-name/quarantine

Security Checklist​

Before approving a server, verify:

  • Source: Is the server from a trusted source?
  • Code Review: Have you reviewed the server's code?
  • Tool Descriptions: Do tool descriptions look legitimate?
  • Network Access: Does the server need network access?
  • Permissions: Are requested permissions appropriate?

Detection Patterns​

Tool-description analysis is performed by the deterministic, fully-offline detect engine that backs the built-in tpa-descriptions scanner. It runs seven checks across two tiers — four hard checks that auto-quarantine and block approval, and three soft checks that raise a human-review item:

CheckTierCatches
unicode.hiddenhardZero-width / bidi / TAG-block / PUA character smuggling
shadowing.cross_serverhardImpersonation clone (same name + near-duplicate description on another server) or exclusive cross-server reference
payload.decodedhardbase64/hex blob that decodes to a shell/exfil command
phrase.injectionhardCurated instruction-override / exfiltration directives
directive.imperativesoftInjection directives, secrecy imperatives, instruction overrides
capability.mismatchsoftCompute/string tool touching ~/.ssh etc.; unexplained data-sink param
secret.embeddedsoftHardcoded live credential (confidence-scored, placeholders dropped)

Each check is deterministic and reliability is enforced by a CI eval gate. See Tool Scanner for the full rule reference, the two-tier model, normalization, and the eval gate.

Best Practices​

  1. Review All Servers: Never auto-approve servers added by AI agents
  2. Source Verification: Only approve servers from known, trusted sources
  3. Minimal Permissions: Prefer servers with limited, specific capabilities
  4. Regular Audits: Periodically review approved servers
  5. Network Isolation: Use Docker isolation with network_mode: "none" for untrusted servers

Tool-Level Quarantine​

In addition to server-level quarantine, MCPProxy provides tool-level quarantine that detects changes to individual tool descriptions and schemas using SHA256 hashing. This protects against "rug pull" attacks where a previously trusted server silently modifies tool behavior.

See Tool Quarantine for complete documentation on:

  • SHA256 hash-based tool approval
  • CLI commands: mcpproxy upstream inspect and mcpproxy upstream approve
  • Configuration: quarantine_enabled (global) and auto_approve_tool_changes (per-server; deprecates skip_quarantine)
  • REST API endpoints for tool approval management

Durable tool baseline​

A trusted (non-quarantined) server whose tools were never approved treats its first non-empty tool list as its baseline: those tools are approved automatically (approved_by: auto-baseline). From then on, under manual trust, every new or replaced tool waits for review.

MCPProxy records that the baseline was decided in its database, per server (bucket tool_baseline_decisions), so the decision survives everything that removes the approved tool records:

  • the server replaces its whole tool list (for example read_note disappears and wipe_all_notes appears): the new tool stays pending, on every later discovery and after a restart, and cannot be called until approved;
  • an explicit refresh that returns zero tools, followed by new tools: the same;
  • a quarantined server approved from an empty review, or approved "without seeing tools": tools it lists after the approval are pending.

The record is written by the first trusted discovery that approves a tool, by every server approval from the review screen, CLI, tray or POST /api/v1/servers/{id}/security/approve, and back-filled on the first discovery after an upgrade for servers that already have approved tools. It is removed only when the server itself is removed (or pruned from the config), so a server removed and added again under the same name starts with a fresh baseline. A server whose first tool list is empty gets no record; its first non-empty list is still its baseline. If the record cannot be read, or the server's earlier approval records cannot be listed, or the record cannot be written for a server that already has approved tools (the back-fill), the discovery pass is skipped before it writes anything: nothing is removed, nothing new is indexed, no approval record changes, and the server keeps the tool list of its last applied discovery until the next pass retries. A failed write on a server's first trusted discovery (no approved tools yet) does not skip the pass: its tools are approved and indexed as usual, and those approved tools are themselves the baseline, so the next pass writes the record before it can remove or auto-approve anything.

Tool lists are applied in the order they were captured​

Several things re-list a server's tools: its connection, a notifications/tools/list_changed, the periodic sweep, an explicit refresh, the reindex after an approval or block, and the review capture of a quarantined server. They can finish in any order. MCPProxy numbers every upstream tools/list when it is sent, and applies a server's tool list only if no newer one was applied before it. An older list that arrives late changes nothing: not the review records, not the search index, not the tool hints that decide which call_tool_* variant may run a tool, and not the stored tool list the next reindex starts from. For example, a sweep that listed read_note as read-only cannot undo a later refresh that found it destructive and held it for review.

The same rule covers a list that came from a connection that is gone: when a server's endpoint is replaced or the server is removed, or it reconnects, a list still in flight from the old connection is dropped. After a reconnect the reindex that follows an approval lists the server again instead of reusing the tool list the old connection returned. A list that came back empty and was ignored as a transient blip (connect and tools/list_changed) still counts as the newest list: an older non-empty one is dropped after it.

When a tool list cannot be fully recorded​

Applying a tool list writes several things: the review records, the removal of tools the server no longer lists, the search index and the tool hints that decide which call_tool_* variant may run a tool. If one of those writes fails (for example the database or the index cannot be written), the list counts as not recorded. MCPProxy then:

  • keeps the server's tools listed and describable, but refuses every call to them with tool definitions could not be recorded; retry the capture, so a tool that became destructive cannot run under its old read-only hints;
  • refuses to approve or enable tools of that server, and to approve the server itself (REST answers 503 with the same text). Disabling and blocking still work;
  • does not publish anything from the failed list.

The next tool list that is recorded completely (the next refresh, reconnect or sweep) lifts this. A transient empty list does not. The state is kept in memory only: after a restart the server is listed again before any of its tools can be called.

Tools a server stopped listing are removed from its approved records even when an earlier removal failed part-way, or failed before a restart. Pending, changed and disabled records of such tools are kept. A disabled or quarantined server keeps no tool list in memory, so re-enabling it shows its tools only after they are listed again.

Trust modes (auto | scan | manual)​

Since spec 086, each server carries a trust mode that governs both new-server admission and tool-change approval (superseding the binary auto_approve_tool_changes flag, which is migrated onto it automatically):

ModeAdd timeTool changes
autoAdmitted without quarantine or scanningTrusted without scanning (rug-pull risk)
scan (default)Quarantined, then a fail-closed automatic TPA scan admits it on a clean verdictNew tools and changed tool contracts are auto-approved when the offline scan verdict is clean with full coverage (spec 119) and the structural gate finds nothing (spec 120): any safety-hint change in either direction, a new required parameter, a type change, an enum member added or an enum dropped, a loosened or removed constraint, a default or const added or changed, a new parameter that carries a default, const or enum, an output-schema change, or a dispatch-tier change with an equal hash is held whatever the verdict (details). A finding or an uncovered tool is held for review with the evidence attached. A paraphrased rug pull that scans clean and changes nothing structural is admitted: that is the accepted residual risk of the default tier — use manual on servers where it is not acceptable
manualQuarantined for human reviewEvery change held for review, including input-schema-only changes, an added output schema, an added description and annotation-hint flips (see What quarantine does and does not cover); a clean scan verdict is recorded as evidence and approves nothing

Config field: per-server trust_mode; REST: trust_mode on POST/PATCH/GET /api/v1/servers; CLI: mcpproxy upstream add --trust-mode.

Unrecognized values are rejected, not guessed. The values are case-sensitive (Scan is not scan). A bogus value is refused with a 400 naming the accepted vocabulary on POST/PATCH /api/v1/servers, by the upstream_servers MCP tool, and by --trust-mode / upstream add-json — every seam where a value is being written.

Loading is different: it normalizes instead of failing. A bogus trust_mode already sitting in mcp_config.json (older releases persisted whatever the API was handed) is rewritten to manual at load time with a WARN on stderr naming the server and the offending value. This is the tier the runtime already applied to it, so nothing about the decision changes — but the daemon still starts and hot-reloads instead of refusing to boot on a file it cannot fix itself. POST /api/v1/config/apply normalizes the same way, so a full-config apply is never blocked by a legacy value it did not introduce. Should an unvalidated value ever reach the runtime anyway, resolution still fails closed to manual.

Automatic informational baseline scan​

Independently of trust_mode, MCPProxy runs the free in-process Pass-1 TPA scan so every server ends up with a security verdict instead of an empty badge:

  • On admission — a newly added, enabled server gets one baseline scan. Servers with trust_mode: "scan" are skipped entirely: that mode's own admission gate scans them, and routing an informational verdict into its settle-driven auto-approval could change quarantine state.

  • Once per installation — a background sweep at startup scans enabled servers that have never been scanned (for installs that predate this behaviour). It is serialized, never delays startup, is cancelled on shutdown, and a persisted marker keeps it one-shot.

  • After a tool definition changes — when a tool's definition changes, or a tool is added to a server that already has an approved baseline, in every trust mode except auto, two things run without a click:

    • the in-process scan of that tool's current definition, recorded on the tool (see Hold evidence in the Web UI). In scan mode this scan is the gate's own; in manual mode it is run for evidence only. It re-runs each time the held definition changes again.
    • a debounced (about 5 seconds, coalesced per server) informational rescan of the server, so the review screen's scan line returns to current instead of stale ("Scan out of date: N tool definitions changed after the last scan"). A change that lands while a scan of that server is queued or running is retried, not dropped. Disabled servers, trust_mode: "auto" servers and quarantined trust_mode: "scan" servers (the admission gate owns those) are skipped. Failed starts share the per-server retry cap of the admission scan.

A scan never reconnects a server that is waiting on a person's OAuth login (a login flow in flight, or a server parked in Pending Auth): reconnecting replaces the server's instance, which retires the pending flow and turns the browser approval into a failed code exchange (GH #1722). Such a server is scanned with what can be exported without a restart (the tool contracts discovery already recorded, if any) and the reason is logged. The temporary review connection a newly quarantined server gets is likewise held open while a login for it is in flight, so its expiry cannot disconnect the server underneath the sign-in. Neither rule approves, unquarantines or blocks anything.

These scans are informational: the verdict fills in the scan summary and the UI badge and never quarantines, approves, or blocks anything. The approval decision belongs to the trust-mode gate, which runs its own synchronous scan of the changed definition: under scan a clean change is approved there (spec 119), under manual nothing is approved automatically and the gate's scan only adds evidence. Disable everything above with security.auto_baseline_scan: false (env: MCPPROXY_AUTO_BASELINE_SCAN), or keep the admission scan and turn off only the per-change rescans with security.auto_rescan_on_change: false (env: MCPPROXY_AUTO_RESCAN_ON_CHANGE). Manual scans keep working either way. The trust_mode: "scan" gate above is a separate path: its admission-time scan of new tools is not switched off by either setting.

Signature bundle (offline TPA corpus)​

The scan mode runs an offline TPA signature corpus (the tpa-db scanner-bundle.json). By default it is the corpus embedded in the build; set security.tpa_bundle_path in mcp_config.json (env override: MCPPROXY_TPA_BUNDLE_PATH, which wins over the file value on every path — loader, hot-reload, /api/v1/config/apply) to run a corpus from disk instead. The path is re-read on config hot-reload, so refreshing signatures needs no restart, and it is honoured in every transport, stdio included.

A configured bundle that cannot be read, parsed, version-checked, or compiled is refused: the previously active corpus stays live (fail-closed, never fail-empty) and the reason is logged and reported as load_error below. A bundle that parses but contributes zero runnable rules — an empty rules array, or rules that are all non-runnable offline — counts as a load failure for exactly the same reason: an empty corpus would leave the scan gate reporting full coverage while nothing at all was being matched.

Which corpus is live is visible in:

  • mcpproxy security overview — source, version, freshness stamp, fingerprint, and the runnable / skipped / declared-skipped rule split;
  • mcpproxy security bundle / GET /api/v1/security/bundle — the full status: full digest, sequence, signing authority, trust epoch, whether the signature verified, staleness, degraded coverage, and the last refused candidate;
  • GET /api/v1/security/overview → signature_bundle;
  • the Web UI Security tab ("Signatures (runnable)" stat).

Signed bundles (Spec 101). A bundle can carry a detached Ed25519 signature (<bundle>.sig). A signed bundle is checked against the keys you trust (tpa_operator_signers) and against a per-install anti-downgrade watermark, so an older genuinely signed corpus cannot be put back. Rejections keep the previous corpus serving and are reported as last_rejection. Unsigned bundles still load unless require_signed_bundle is on, but they always run with degraded coverage. The bundle is not watched: it activates on config reload, restart, or mcpproxy security bundle reload.

Degraded coverage suspends scan-mode auto-approval. When MCPProxy cannot vouch for the corpus that is scanning (an unsigned external bundle, an operator bundle with no baseline, a lost operator bundle, or an embedded corpus older than a bundle seen before), new and changed tools are held for human review with held reason scan_coverage instead of being approved on a clean verdict. The reason is always shown in the status. An ordinary fresh install on the embedded corpus is not degraded.

Rollback is by the full SHA-256 digest, not a path or the 12-character fingerprint (mcpproxy security bundle rollback). The operator lifecycle, every rejection and degraded reason, key compromise and the threat-model boundary are in Signed TPA Bundles.

Daily refresh and optional fetch (Spec 087). The configured bundle file is re-read about every 24 hours (security.tpa_refresh_interval), so a dropped file activates without a restart. A fetch of a signed bundle from a URL you choose (tpa_fetch_enabled, tpa_fetch_url) is available but off by default, with no default URL, and today works only with operator-signed bundles because the publisher key set is still empty. A fetched bundle passes the same checks as a file. mcpproxy security bundle refresh runs one cycle now; reload never touches the network. After a new bundle activates, approved tools are re-checked against it: see Re-protection after a bundle activates.

The "Add time" column applies to every admission path — the upstream_servers tool, the REST API, a registry add, and (since issue #937) a first-seen server in mcp_config.json. See Servers added by hand-editing mcp_config.json.

Web UI (spec 088): the server's Configuration tab has a tri-mode selector (choosing auto asks for confirmation and explains the risk); the add-server form chooses the mode instead of a raw quarantine checkbox (initial quarantine is derived from the mode); server tiles show a trust-mode badge.

Re-protection after a bundle activates​

A tool you approved last month was checked against the corpus that existed then. When a newer signature bundle activates (a refresh, a fetch, a dropped file or a rollback), that tool may now match a signature that did not exist when you approved it. MCPProxy re-checks your approved tools against the new corpus without waiting for them to change, and only ever adds evidence for a human.

The sweep runs once per activation (a given bundle digest is swept once; a rollback is a new activation and one more sweep). It does not run for a last-known-good restore at startup or for a refused candidate. If the last completed sweep covered a different bundle than the one now active (an activation before the app was fully up, a crash mid-sweep, an upgrade that changed the embedded corpus), a startup catch-up runs it once. It has two legs:

  • Held-tool evidence. For every server with pending or changed tools, quarantined servers included, the scan of the stored current definition is repeated under the new corpus and the evidence shown on the tool is updated (see Hold evidence in the Web UI). A signature that the newer corpus no longer has leaves the tool held, with a clean verdict that still waits for your review.
  • Cached scan of the stored tools. For every non-quarantined server with at least one approved tool, in any trust mode, an informational scan runs over the stored current definition of every tool record, approved and held alike, so the result describes the same tools a live scan does and never replaces a verdict that rests on a held tool with a clean one. The report, scan summary, server-list badge, Security overview and activity log show the result, and the tool's scan verdict on the server's review view (GET /api/v1/servers/{name}/review) reflects a finding, naming the matching signature as tpa.<id>.<detector>.

What it never does. It never approves, blocks, revokes, disables or unquarantines anything, and a clean verdict from this path approves nothing. An approved tool stays approved with the same approved hash, a held tool stays held (under scan the next discovery pass may approve it if its current definition scans clean), a quarantined server stays quarantined. The cached scan skips quarantined servers on purpose (checked again at the moment each server's scan starts), and the settle event of a re-protection job is ignored by the scan-mode auto-approval and the review capture (both belong to quarantined servers).

Cached only. The sweep reads what MCPProxy already stored. It does not connect to the upstream server, does not call tools/list, starts no Docker deep scan, is not counted in the scan telemetry, and cannot be influenced by a slow or hostile server. A server with no stored approved contracts is reported as unscanned, never as clean.

Pacing. Servers are swept one at a time, with a short pause between them. A server that is busy with another scan is retried without counting as a failure; a server whose scan cannot start is tried at most three times. One sweep produces at most one finding per tool and one activity record per server, so a large install does not get an alert storm.

It depends on security.auto_baseline_scan. The cached scan is an informational scan, so it follows the same switch (env MCPPROXY_AUTO_BASELINE_SCAN). With it off, the held-tool evidence still refreshes, the cached scan does not run, and the status says so (reprotect.state: suspended, reason: auto_baseline_scan_off). Turning the switch back on resumes the suspended sweep without a restart.

Where to see it. mcpproxy security bundle prints a Re-protection: line (state, how many scanned servers matched the active corpus, when it last completed), and GET /api/v1/security/bundle and /api/v1/security/overview carry the same reprotect object (idle, running, suspended or failed). There is no new attention or review-queue item for "an approved tool now matches a signature": a server with only approved tools does not enter /review or the Home attention list. The finding is visible on the surfaces above. Whether to add an attention kind for it is an open decision, since it touches every surface of the Web UI and the macOS app.

Limitation. In stdio mode there is no scanner service, so the cached scan of approved tools does not run; the held-tool evidence refresh still does.

Hold evidence in the Web UI (spec 088)​

When the scan gate holds a tool, the approval record carries evidence — held_reason (scan_findings = the scan found threats vs scan_coverage = the scan could not complete, held as a precaution), held_verdict, and held_signals (matched check ids, with known-attack TPA-YYYY-NNNN signature ids listed first). The Web UI renders this on the tool-quarantine panel, the change-diff dialog, and the global Tools page, with a best-effort link into the server's latest scan report highlighting matching findings. The quarantine banner distinguishes four states: scan running, scan verdict blocked automatic approval, scan could not complete (retry offered), and awaiting manual review.

held_signals can also carry the structural gate's own ids (spec 120), listed ahead of the scanner's: GATE-HINT-SAFER-FLIP (the tool re-labelled itself as safer), GATE-HINT-CHANGE:<hint>:<old>>:<new> (any safety-hint change, also GATE-HINT-CHANGE:dispatch:<old>>:<new> when the call_tool variant moved), GATE-SCHEMA-SHAPE:<reason>:<path> (for example GATE-SCHEMA-SHAPE:new-required:bcc) and GATE-OUTPUT-SCHEMA:added|removed|changed. They are recorded with held_reason: scan_findings and the scan's verdict as it was (clean when the scan found nothing), so the review screen says why the clean-scanned change is still held. A very large delta is compacted per rule (GATE-SCHEMA-SHAPE:more:+7) so the record's 16-signal cap never hides a rule. The Web UI labels them ("Input schema widened: new-required bcc"); the CLI prints the raw id.

held_reason has a third value, awaiting_review, recorded in manual mode when the automatic scan of a changed or newly pending tool's current definition came back clean (held_verdict: "clean", no signals). It is evidence, not a finding and not a precaution: the tool is still changed or pending and stays blocked until you approve it, because under manual a clean verdict never approves a changed contract (under scan the same verdict approves the tool, so nothing is held). The Web UI shows it as "Scan clean, awaiting your review", the review screen shows the tool's scan verdict as clean (the tool still starts unchecked there: the in-process verdict never widens what is pre-selected), and mcpproxy tools list shows clean in the HELD column. The server banner is derived from the server scan, not from this field, and is unchanged.

Reviewing a held change​

Every changed tool contract is held in manual mode; in scan mode (the default) a change is held when the offline scan flags it, cannot cover it, or the structural gate returns a signal (a hint change, a widened input schema, an output-schema change). To keep review cheap, the Web review screen and mcpproxy review show <server> show what differs from the approved definition: the description, parameters added, removed, retyped or newly required, output schema changes, and flips of the four safety hints (readOnlyHint, destructiveHint, idempotentHint, openWorldHint). Approve update (N changed tools) on the Web review screen approves exactly those tools in one click. The approval is bound to the definitions and the server incarnation on screen, so if a tool changes again, or the server is removed and re-added, between viewing and clicking, the request is refused with 409 and nothing is approved; reload and review again.

A server you trust to update freely can opt out of the hold with auto_approve_tool_changes: true on its entry (or trust_mode: auto). The hold messages in the Web UI, the CLI and the refusal an MCP client receives name this option.

Structural changes are held​

Since spec 120 the scan gate compares the changed contract with the approved one before it trusts a clean scan. The comparison is deterministic and structural (no word list, no similarity score, no classifier): it reads the four safety hints and walks both schemas keyword by keyword. Any of these holds the change whatever the scan says, with the signal recorded on the record:

SignalHeld when
GATE-HINT-CHANGEthe effective value of readOnlyHint, destructiveHint, idempotentHint or openWorldHint changed in either direction (MCP defaults applied); the two safer flips also carry GATE-HINT-SAFER-FLIP. openWorldHint true→false matters most here: it marks the tool's output as trusted. Also when the call_tool variant the tool is routed to moves while the hash does not (an explicit destructiveHint: true dropped to no hints routes the tool as read)
GATE-SCHEMA-SHAPEa new required entry; a type or $ref change; an enum that gained a member or was dropped; any pattern or format change; a minimum/maximum/length/items/properties bound loosened or removed; additionalProperties: false relaxed; a default added or changed, a const added, changed or removed; a new property (or $defs entry) whose subtree carries a default, const or enum; anyOf/oneOf gained a branch, allOf lost one; the approved input schema is unknown (a record from before full capture)
GATE-OUTPUT-SCHEMAthe output schema was added, removed or structurally changed (descriptions and titles inside it are ignored)

Narrowing edits pass and leave the decision to the scan: a property removed, a required entry removed, an enum member removed, a bound tightened or added, additionalProperties set to false, a default removed, and a new optional property that carries no default, const or enum. Text keywords (description, title, examples) are the scanner's business everywhere. The walk recurses through properties, items, $defs, anyOf/oneOf/allOf and patternProperties, so a widening three levels down is held like one at the root, and every structural step is compared with the last approved contract, so splitting a change across releases does not help. The same rules are pinned by a fixture shared with the Web review differ, so a held change is always visible on the review screen.

What the gate does not catch, said plainly. A well-written paraphrase of a description, a one-word edit, slow text drift over several releases, a server whose behaviour changes while its definition does not, and structural keywords the walk does not name (dependentRequired, if/then/else, contains, uniqueItems). No offline gate catches those. The controls that remain are trust_mode: manual on the servers you do not trust, a profile max_tier, the response pipeline, and the per-token rate limit.

Audit. Every automatic approval of a changed contract (tool_auto_approved, scan-approved or auto_approve_tool_changes) carries the contract it replaced: old_hash, old_description, old_schema, old_output_schema, old_annotations, the new values, approved_by, the gate signals evaluated (gate_signals, empty on an approval), and for a scan-approval scan_verdict and scan_bundle (the active TPA bundle), so what the gate let through can be reconstructed afterwards from mcpproxy activity list.

Upgrade. A server whose trust_mode is empty runs under scan by default. The first start after the upgrade logs one WARN naming every such enabled server (persisted marker in config.db; a fresh install with no servers logs nothing), and mcpproxy doctor names them on every run. Set trust_mode: "manual" on a server whose tool changes a person should review.

Block (approve + disable)​

When reviewing a pending or changed tool you may want to acknowledge it but keep it hidden from MCP clients — for example, dismissing a noisy "changed" flag for a tool you never intend to use. The block operation does this atomically: it approves the tool (clearing the quarantine flag) and disables it in a single, all-or-nothing server-side write, so a tool is never left in the approved+enabled state.

  • REST: POST /api/v1/servers/{id}/tools/block with {"tools":[...]} or {"block_all": true}.
  • MCP: quarantine_security operations block_tool (with name + tool_name) and block_all_tools (with name).

A blocked tool can be re-exposed later with the normal enable operation (POST /api/v1/servers/{id}/tools/{tool}/enabled with {"enabled": true}).

Namespaced tool names​

A tool's identity is the exact name its server reports in tools/list, colons included. erase and ns:erase on the same server are two tools, and each has its own approval record, index entry and callability — a record for erase never approves ns:erase, and vice versa. Every gate (dispatch through call_tool_*, direct-name dispatch on /mcp/all, call_tool() inside code execution, preflight and describe_tool) looks the record up under the exact name it will dispatch.

Releases before this rule filed a colon-named tool under the text after its first colon, so ns:erase shared the record erase. On the first discovery after upgrading, a server that has a baseline (any approved tool) and serves colon-named tools therefore sees those tools become pending once, under their own names on manual and scan trust — review them with mcpproxy upstream inspect <server> and approve them with mcpproxy upstream approve <server> <tool>, the quarantine_security MCP tool, or the Web UI. trust_mode: auto servers (and installs with quarantine_enabled: false) auto-approve them. Nothing is deleted: the old collapsed record stays with the bare name it stores.

Two things about the old record carry over so an upgrade never silently widens access:

  • A user block (a Disabled toggle, disable_all, block_all) on the collapsed record is copied onto the namespaced tool's new record, and the log says so at WARN naming both keys. The namespaced tool stays hidden until you enable it under its own name — a tool you had blocked before the upgrade is still blocked after it. A quarantine lock (pending / changed) on the collapsed record is adopted onto the new record with its evidence (the before/after definition of a rug pull, the scan verdict of a held tool) on manual and scan trust — including the first discovery of a server that has no baseline yet, where a brand-new tool would be auto-baselined: a tool the old release held for review stays held until you approve it by its own name, and the review shows what the old release had flagged rather than "new tool". The lock is never turned into a block: approving the tool by its name is all it takes (no second enable toggle). Under trust_mode: auto or quarantine_enabled: false the old lock never bound and the tool auto-approves as before.
  • A namespaced tool you had toggled in the UI before the upgrade already has a record under its exact name — one that the toggle created without an approved contract hash. Such a record carries no approval decision of its own, so on the first discovery after upgrade it takes its decision from the collapsed record: a pending / changed lock there (with its before/after evidence) is adopted and the tool stays held for review under its own name; an approval there counts only if the definition it approved is the one the server reports now — then the exact record is baselined and change detection works from then on — while a differing definition is held as changed with the approved one as the before-evidence, exactly as the collapsed record itself would have been (a tool that changed while it had no live baseline is a potential rug pull, not a new baseline). A user block on the collapsed record rides along on every one of those outcomes. With no collapsed record at all the tool is pending under its own name on manual and scan trust (a green scan approves it on scan), and baselined on trust_mode: auto or with quarantine_enabled: false. Nothing you toggled is ever approved for a definition nobody reviewed.

The same rule now applies to the toggle itself: disabling or re-enabling a tool that has no approval record yet (a tool discovery listed but never filed) creates a pending record while the quarantine gate is active for its server — the toggle records visibility, it does not approve — so a disable → enable round trip leaves the tool pending until you approve it by name. Under trust_mode: auto or quarantine_enabled: false the record is approved and baselined to the definition the server currently reports.

Only records written by an older release are consulted this way. Every record this release writes is stamped as identity-keyed, and the first discovery pass after upgrade stamps every remaining record the server holds — including collapsed records for tools the server no longer lists — so the migration runs exactly once per server: a genuine sibling erase you disable later never affects a new v2:erase. The one exception is an old record that still restricts — user-disabled, pending or changed — for a tool the pass did not list (the tool was not served on the first pass after upgrade, or an authoritative empty refresh dropped it). Such a record is a decision you or the old release made about a tool that is merely absent, so it is left unstamped and waits: when the tool reappears, the pass that files its exact record consults the old record once — the block or lock carries over exactly as above — and stamps it then.

Disabling Quarantine​

Not recommended, but you can opt out of quarantine globally by setting a single top-level flag in ~/.mcpproxy/mcp_config.json:

{
"quarantine_enabled": false
}

When quarantine_enabled is false:

  • Servers added dynamically via the upstream_servers MCP tool or the POST /api/v1/servers REST endpoint default to not quarantined.
  • Tool-level quarantine (per-tool SHA-256 approval of descriptions and schemas, see Tool Quarantine) is skipped.

An explicit quarantined field in an add-server request still wins over the default, so client code can always override on a per-server basis. Per-server auto_approve_tool_changes: true auto-approves all post-baseline tool changes and additions for that server (the deprecated skip_quarantine: true is migrated onto it automatically).

Warning: Disabling quarantine exposes your system to Tool Poisoning Attacks. Only do this on machines where every MCP server you connect to is already trusted.