Skip to main content

MCP Cooperative Guard

The check tool in @controlzero/mcp-server brings centrally managed Control Zero DLP rules into any compatible MCP client. It fetches the project's signed policy bundle, scans a proposed tool call locally, and returns an allow, warn, or deny decision before invocation. The text being checked stays on the machine.

It is the third leg of the Control Zero enforcement triangle:

The MCP guard is cooperative: the client remains responsible for honoring the returned decision. Use the SDK or gateway as the enforcement point when a deny must stop execution.

Why cooperative?​

MCP Guard covers clients where the SDK is not embedded in the agent process and the request is not routed through the gateway. It gives those clients a local DLP decision before a tool call.

Hard enforcement uses either:

  • The Control Zero SDK in the same process as the agent (Python or Node), OR
  • The Control Zero gateway as an HTTP proxy in front of the LLM provider URL.

For a desktop chat app that runs a provider API directly, MCP Guard surfaces the organization's DLP decision without requiring an SDK inside the client or a gateway in the request path.

Install​

Configure the Control Zero registry once: add @controlzero:registry=https://npm.controlzero.ai to your .npmrc (or run npm config set @controlzero:registry https://npm.controlzero.ai). It applies to npm install and npx for the whole @controlzero scope.

Use version 2.0.1 or later. In 2.0.0, check failed on every call for an org with DLP enabled.

npm install -g @controlzero/mcp-server

The package's stdio binary is controlzero-mcp. Without a global install, run it as npx -p @controlzero/mcp-server controlzero-mcp.

Configure​

check needs an enforcement key (cz_live_* or cz_test_*) in CONTROLZERO_API_KEY. The project is resolved from the key. The management tools in the same server need a separate management key; see MCP Server.

Claude Desktop​

Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):

{
"mcpServers": {
"controlzero": {
"command": "controlzero-mcp",
"env": {
"CONTROLZERO_API_KEY": "cz_live_xxxxxxxxxxxxxxxx"
}
}
}
}

Restart Claude Desktop. The tools appear in the tool picker.

Other clients​

Any client that supports MCP stdio servers works: the command is controlzero-mcp (or npx with -p @controlzero/mcp-server controlzero-mcp) and check needs only CONTROLZERO_API_KEY. CONTROLZERO_API_URL overrides the API base URL (default https://api.controlzero.ai).

The check tool​

Inputs:

  • tool_name -- the tool the client wants to invoke (for example bash, read_file, fetch)
  • tool_args_text -- a flat text rendering of its arguments (join string arguments with newlines)
  • scope -- optional: mcp_guard (default) or sdk, the surface whose rules apply

Output (abridged):

{
"decision": "deny",
"reason_code": "DLP_BLOCKED",
"complete": true,
"rule_matches": [
{
"rule_id": "abc-123",
"rule_name": "US Social Security Numbers",
"category": "pii",
"action": "block",
"matched_text": "123-45-6789"
}
],
"uncompilable_rule_ids": [],
"held": []
}

Decisions:

reason_codeMeaningDecision
NO_RULE_MATCHEvery candidate rule ran and none matched.allow
NO_ACTIVE_POLICIESThe surface declares no controls and no rule applies here.allow
COVERAGE_HELDEvery control declared for this surface is held; see held.allow
DLP_DISABLEDDLP is not enabled for the org.allow
RULE_MATCHA mask or detect rule matched. For mask, masked_text carries the masked text.warn
DLP_BLOCKEDA block rule matched. The client should not invoke the tool.deny
INCOMPLETE_EVALUATIONSomething the org declared was not evaluated; reason says what.warn

An allow is never presented as a clean scan when it was not one: if complete is false and nothing matched, the decision becomes warn with INCOMPLETE_EVALUATION. A match keeps its own code (RULE_MATCH or DLP_BLOCKED) and still reports complete: false, so read complete as well as reason_code. Causes include a rule whose pattern cannot run in this client (uncompilable_rule_ids lists every rule not evaluated), a rule that needs context-keyword proximity, coverage counts that do not reconcile with the rules delivered, and a policy bundle that changed while the check was running.

Patterns and prompts​

Pre-flight check before a destructive command​

Before running any shell command that touches /etc, /var,
or any file owned by another user, call check with
tool_name="bash" and tool_args_text set to the proposed command.
If decision is deny, abort and show the rule name to the user.
If decision is warn, show the reason and ask the user for explicit
confirmation before proceeding.

Mid-stream paste check​

Whenever the user pastes text containing what looks like an SSN,
credit card, API key, or email, call check with
tool_name="paste_check" and tool_args_text set to the pasted text
before proceeding. If decision is deny, refuse to use the
content and explain why.

Security model​

check only reads. With an enforcement key it cannot change rules, publish, or approve anything.

The text in tool_args_text is not sent to Control Zero. The server pulls the project's policy bundle, verifies its signature, decrypts it with keys fetched for the enforcement key, and evaluates the rules in a separate worker thread inside the MCP server process on your machine. With DLP enabled, a bundle that fails verification is an error, never an allow. With DLP off for the org, check answers DLP_DISABLED without needing the bundle.

Each call makes a conditional request for the bundle, so a steady-state check does not download it again, and re-reads the effective DLP state for the project.

Troubleshooting​

Every call fails with an authentication error​

CONTROLZERO_API_KEY is missing, revoked, or not an enforcement key. A management key (cz_mgmt_*) is refused by check. Create an enforcement key in the dashboard.

INCOMPLETE_EVALUATION with a reason about the policy bundle changing​

A policy change landed while check was running. Call it again.

A newly published rule is not matching​

check reads the latest bundle the API has built for the project, so the first call after the bundle is rebuilt picks the rule up. A bundle cached by the MCP server is re-fetched in full at least every five minutes.

"Tool not found: check"​

Confirm the client runs version 2.0.1 or later: npm list -g @controlzero/mcp-server. The guard_* tools from earlier versions no longer exist.