Discover servers
guard mcp-inventory reads the MCP servers configured in each harness, including JetBrains AI Assistant's settings XML. guard mcp-tools reads the same servers. Both take --project DIR, --discover and --no-project.
--discover adds the projects harnesses have recorded and walks the catalog's search roots for more. Add --search-root DIR, --search-depth N (default 4, at most 8) or --max-projects N (default 256, at most 4096) to widen it. The discovery block, also in guard mcp-tools and guard scan output, names any bound that left projects out, and the Guard Cloud upload is then marked truncated.
Add --registry to classify each server against your organization's signed reference set. See Registry and shadow servers. Nothing is contacted.
Pin tools
An approved server can rename a tool, rewrite its description or widen its schema without its configuration changing. guard mcp-tools records each server's tools when you approve them and reports changes after that.
guard mcp-tools check # baselines only; contacts nothing
guard mcp-tools check --remote # also query remote servers
guard mcp-tools check --launch-stdio --server docs
guard mcp-tools check --definitions docs=tools.json
guard mcp-tools approve docs --remote --expect sha256:...
guard mcp-tools list
guard mcp-tools remove stdio:3f2a91c0
Output is JSON.
Getting definitions
By default nothing is contacted or launched. Choose a source explicitly:
| Option | What it does |
|---|---|
--remote |
Asks http:// and https:// servers for tools/list. It never sends tools/call. Header references such as ${TOKEN} aren't expanded, so servers that need them, or OAuth, report HTTP 401. |
--launch-stdio |
Launches stdio servers with a minimal environment and stops them once the tools are listed. |
--definitions SERVER=FILE |
Reads definitions from a file. Works air-gapped. For approve, use --definitions FILE. |
Warning:
--launch-stdioruns the configured command, which is third-party code, with your account's access. Only use it for servers you'd let your harness start.
--server SERVER limits check to named servers. Disabled servers are only retrieved when named.
Server identity
Baselines are keyed by what a server is, so a changed configuration needs a new approval:
- Stdio (
stdio:...): the harness, the config file and entry, the server name, the command, and the launcher package with its version. Upgrading the package needs a new approval. - Remote (
remote:...): the URL's scheme, host and port, plus a digest of its path. The query is left out.
Servers with neither a command nor an http:// or https:// URL aren't listed. Output never includes URL paths, arguments, header values or environment values.
Check results
status |
Meaning |
|---|---|
not_retrieved |
Nothing was contacted or launched. |
unpinned |
Retrieved, with no baseline yet. |
unchanged |
Matches the baseline. |
changed |
Differs from the baseline; see findings. |
unreachable |
Retrieval failed. |
baseline_outdated |
The baseline uses an older canonical form. Review and approve again with --expect. |
Findings are tool_added, tool_removed, tool_description_changed, tool_schema_changed, tool_annotations_changed and server_unreachable. They carry digests, not descriptions or schemas. --show-definitions prints the current definitions for review in the terminal; don't store or upload that output.
Each tool is pinned by its name, title, description, inputSchema, outputSchema and annotations. _meta and icons aren't pinned.
Tool poisoning checks
check and approve also match definitions against the catalog's mcp_tool_rules, looking for text that tries to steer the model:
category |
Looks for |
|---|---|
hidden_characters |
Tag characters, bidirectional overrides, zero-width characters, terminal escapes |
naming |
Tool names outside A-Z a-z 0-9 _ - . or over 128 characters |
hidden_instructions |
HTML comments, and tags such as <IMPORTANT> or <system> |
auto_invocation |
"Always call this tool first", even if the user didn't ask |
concealment, consent_bypass |
"Do not tell the user", or acting without confirmation |
credential_access, exfiltration |
Reading or sending SSH keys, cloud credentials, .env files or tokens |
cross_tool |
Changing how other tools or servers behave |
policy_override |
"Ignore previous instructions", or switching off hooks or Guard |
A finding is suspicion, not proof. Legitimate servers match too: for example, an SSH server that names ~/.ssh. Paraphrased, translated or encoded instructions can get past the rules. These checks run only in mcp-tools check and approve, not in guard scan or the hooks.
Approve
approve SERVER makes the current definitions the baseline. If nothing can be retrieved, nothing is approved.
--expect DIGESTapproves only if the digest matches the one you reviewed.- If a poisoning rule matches,
approverefuses. After review,--accept-suspiciousallows it, butcheckkeeps reporting the findings. An incomplete inspection can't be accepted this way.
Baselines live in mcp-tool-pins.json in the Guard home, an owner-only file holding digests, not descriptions or schemas.
Tool access rules
The signed catalog's mcp_tool_access section says which tools of which servers each agent may call. The harness hooks enforce it on every MCP call they see.
"mcp_tool_access": {
"default": "block",
"teams": {"docs-writers": ["alice", "bob"]},
"rules": [
{"rule": "docs_search", "decision": "allow",
"server": "remote:5e0c…", "tool": "search", "digest": "sha256:9a41…",
"agents": ["claude_code", "cursor"], "reason": "Reviewed read-only search"},
{"rule": "fs_no_delete", "decision": "block",
"server": "stdio:3f2a…", "tool": "delete_file", "reason": "Destructive"}
]
}
| Field | Meaning |
|---|---|
default |
Required: allow, ask or block. Applies when no rule decides, including when Guard can't tell the server, tool or definition. |
teams |
Team names with their OS account names. |
rule |
Unique id, shown in hook messages. |
decision |
allow, ask or block. |
server |
The key from guard mcp-tools list or check. |
tool |
The tool's name. "*" is accepted only for block and ask. |
digest |
The tool's approved digest. Required for allow. |
agents, users, teams |
Who the rule covers. Empty means everyone. |
reason |
Shown in the hook's message. |
How a call is decided:
- The strongest
blockoraskthat applies wins. - Otherwise an
allowdecides only when itsdigestequals the tool's approved digest. - Otherwise
defaultdecides. - Missing data never counts as a match for an allow.
So a renamed tool, a changed definition or a changed server loses its allow and falls back to default.
Rules come only from the signed catalog, never from a local file. The built-in catalog has none.
Where rules are enforced
| Harness | Block | Ask |
|---|---|---|
| Claude Code | Denied | Asks |
| Codex | Blocked | Warns |
| Cursor | Denied | Asks |
| Gemini CLI | Denied | Warns |
| Windsurf | Blocked | Warns |
| Cline | Cancelled | Warns; access_mcp_resource isn't covered |
| Kiro, Amazon Q Developer CLI | Blocked | Blocked as unsupported |
| Kimi Code, Qwen Code | Denied for reviewed native server/tool names resolved against their own config and pins; fixture-tested | Denied as unsupported |
| Copilot CLI / current VS Code Copilot target | Not enforced | Not enforced |
| Claude Desktop, other VS Code client paths, Zed, Roo Code, Junie, OpenCode, Continue, JetBrains | Not enforced | Not enforced |
| MCP gateway | Refused before it reaches the server | Blocked |
An unsupported ask either warns or is refused, as the table shows. Check the calling adapter's failure and qualification limits before relying on a decision.
Gateway adoption and browser tools
The gateway mediates only entries configured to run through it. A signed catalog sets mcp_gateway_adoption to report or wrap. guard mcp-gateway adopt previews; guard mcp-gateway adopt --yes wraps supported user stdio entries, preserving server identity. Add --guard-command PATH to wrap with another guard executable. An entry that already runs mcp-gateway stdio is never wrapped twice. Project files, remote servers and unsupported or unsafe layouts are skipped.
Inventory reports wrapped, not_wrapped, wrapper_removed or excepted; older uploads are unknown. A server Guard wrapped stays wrapped only while its entry runs exactly the program and arguments Guard recorded. Pointing it at another program, even one named guard, makes it wrapper_removed. An entry Guard didn't wrap counts as wrapped if it runs a program named guard with mcp-gateway stdio, which Guard can't verify. The Console shows states and per-device counts. Anyone who can edit the config can remove a wrapper. Neither a removal detection nor hook installation proves traffic was mediated.
Chrome DevTools MCP
Published chrome-devtools-mcp 1.11.0 and real headless Chrome 155.0.8059.85 have retained macOS evidence from a direct protocol client: routed calls, tool refusals and known-pattern textual redaction. Twelve passing scenarios are separate from four unsupported observations. This does not qualify an agent, model, extension, personal profile, Linux or Windows.
Pin reviewed tool definitions and allow only needed capabilities. Denying navigation, script, interaction or upload tools refuses those calls before the server. Allowed tools can cause browser network effects outside upstream MCP destination policy. Unwrapped MCP and direct CDP bypass the gateway.
The disposable-profile test ran under an outer OS fence. Its qualification limits include a nested-sandbox constraint and unqualified browser capabilities.
Registry and shadow servers
Guard compares configured servers with a public MCP registry and your approved list, and flags servers that are unknown, unapproved, diverge, or have look-alike names.
Devices never contact a registry. Guard Cloud reads the registry and signs a reference set for each device. guard cloud sync fetches and verifies it, and guard mcp-inventory --registry classifies servers offline.
| Classification | Reason | Meaning |
|---|---|---|
approved |
on_approved_list |
Matches an approved server, with no differences. |
unapproved |
not_on_approved_list |
Approval is required and the server isn't on the list. |
unapproved |
diverges_from_approval |
Matches an approved server, but something differs, such as the version. If approval isn't required, the result is known (or unknown) with the same reason. |
known |
in_registry |
In the registry; approval isn't required. |
unknown |
not_in_registry |
Not in the registry or the approved list: a shadow server. |
unknown |
no_package_identity |
A local command Guard can't identify. |
unknown |
reference_* |
No current, verified reference set, so nothing was compared. |
Unknown is never treated as safe.
Servers are identified by package, for the launchers npx, npm exec, pnpm dlx, yarn dlx, bunx, bun x, uvx and uv tool run, or by the remote URL's host. docker, pipx, direct binaries and local paths aren't identified.
Look-alike names are within one edit of an approved or registry name, or equal after folding confusables such as 0/o and rn/m. Names shorter than 6 characters aren't compared.
| ID | Rule | Raised when |
|---|---|---|
| RW-000410 | mcp_server_unknown |
The server is unknown after a comparison. |
| RW-000411 | mcp_server_unapproved |
The server is unapproved. |
| RW-000412 | mcp_server_registry_divergence |
Any divergence is found. |
| RW-000413 | mcp_server_lookalike_name |
Any look-alike is found. |
These are posture findings, always observed. When guard cloud sync, guard cloud mcp-watch or guard cloud mcp-inventory reads the inventory, each detection a server shows for the first time goes into the findings log, so it reaches the Console's findings views and SIEM export. Guard Cloud drops the same detection from the same device within 24 hours as a duplicate. guard mcp-inventory --registry only prints. A signed catalog can reword or leave out these rules, but not rename them.
Set up the registry
- On Guard Cloud, set
GUARD_MCP_REFERENCE_KEYto 32 bytes of hex (openssl rand -hex 32). Without it, Guard Cloud never contacts a registry. - Optionally set
GUARD_MCP_REGISTRY_URL. It defaults tohttps://registry.modelcontextprotocol.io; an HTTPS mirror with the same API also works. - Put the public key from
GET /v1/admin/mcp-registryinto the catalog'smcp_registry.reference_keys, then sign and publish the catalog.
Guard Cloud syncs every 6 hours, or on demand with POST /v1/admin/mcp-registry/sync. A failed sync keeps the previous snapshot. Devices treat a snapshot older than max_age_seconds (2 days by default) as stale, and every server becomes unknown.
Replace the approved list with PUT /v1/admin/mcp-registry/approvals and {"approval_required": bool, "approvals": [...]}. These routes take the administrator key.
What the Console sees
guard cloud mcp-inventory uploads server metadata to Guard Cloud: keyed digests, the matched registry name, harness, transport, classification, and each approved tool's name, definition digest and poisoning verdict (clean, accepted_suspicious, unknown or not_recorded). Commands, arguments, hosts, environment values and tool descriptions, schemas or annotations are never sent. guard mcp-inventory --upload-preview shows what would be sent.
Automatic upload runs on Linux through the packaged user unit, on macOS through a launch agent from guard setup-launch-agent --binary PATH --mcp-watch, and on Windows through a logon task. On macOS and Windows each user still needs their own enrollment; on Linux the user unit drops metadata off locally for the enrolled root sync. The catalog's "mcp_inventory_auto_upload": false turns it off.
The watch also queues bounded MCP activity counts for root sync on Linux; without a running user watch there are no new counts. Its authenticated receiver supplies a local-only launch index so root can observe matching launches without reading users' configs. It does not stop them. macOS and Windows service sensors have no equivalent channel.
In the Console, a block preview shows what a block would stop before anyone adds it: each active device is blocked, excepted, possible, unknown or not_affected, from its newest MCP upload. Each device also says whether its inventory is stale (no contact in 48 hours), which catalog it last applied and whether that's the newest, and, when the target is already blocked, whether its catalog carries the block. From a server, the Console follows it to the devices and agent sessions that last used it; revoked devices stay listed.
Administrators manage the approved list, blocks and exceptions that expire within 30 days, and two actions: unapproved_action for servers not on the approved list, and on_unknown for servers Guard can't identify. Each is flag (the default: the server runs and is flagged), ask or block.
These reach devices only inside a published, signed catalog. A device decides in this order: a matching unexpired exception lets the server run; a matching block refuses it; then unapproved_action, then on_unknown. Harness hooks deny refused calls and ask for ask; guard mcp-gateway refuses for both. With on_unknown left at flag, a block by registry name doesn't match while the device has no current reference set, so that server runs. Harnesses without hooks and servers not run through the gateway aren't stopped. Copilot CLI and the VS Code Copilot target count as harnesses without hooks here, because their hooks can't tell which server a call goes to. A sensor run as the user reports when an agent starts a stdio server the catalog blocks or asks about, as observed, never blocked. The lists show, per entry, how many devices enforce it.
Limits
- Hooks use the approved baseline, not the live definition. A changed server keeps its old allow until someone runs
checkand approves. The MCP gateway checks the definition actually served, but only for servers whose configuration runs through it. - A server can show Guard different tools than it shows the harness. Pinning doesn't prove what the harness received.
- Definitions describe, they don't constrain. Pinning shows a description changed, not what the code does.
- Only tools. Resources and prompts aren't pinned or covered by access rules.
- Approvals are local. A user who can edit the Guard home can change baselines, though not the signed rules.
- Configuration only. A server started outside the harness configs isn't seen. Servers set by a launch flag, an IDE-internal store or an administrator are unknown to access rules. What a launcher downloads isn't checked against the registry.
- Platforms. Registry checks and the Console's MCP page are tested on macOS. Linux runs the same code but wasn't run for this feature. On Windows,
mcp-inventoryisn't qualified and the Console's MCP page isn't built or tested for it. Automatic upload has unit tests only: it hasn't run against a live Guard Cloud, and the Windows logon task hasn't been registered on a Windows host. - Hooks can fail open. Hook timeouts and malformed output let the call proceed in some harnesses.
- Identity limits. Cursor servers launched by command without a name are recorded as
default. - Windows stdio launch.
--launch-stdiofinds a bare command such asnpxonly through the server's ownPATHandPATHEXT, and doesn't launch PowerShell scripts. It worked fornpxanduvxservers on Windows and on Linux under WSL2. A firstnpxlaunch that must install the package can outlast the timeout and leave a brokennpxcache entry, so run such a server once by hand first. When onechecklaunches several servers in a row, a server can sometimes time out.
Written from docs/mcp-tools.md, docs/mcp-registry.md, docs/mcp-gateway.md, docs/chrome-devtools-mcp.md, docs/vscode-copilot.md, docs/native-model-clients.md at commit fb79778d02f0. Rune Guard is in early access.