Set policy on your AI agent. Watch it block a bad action. In about ten minutes.
Install the runtime, write one policy, and watch it stop a real data leak before it happens — then walk away with a signed, verifiable record of exactly what it did. Runs on your laptop. No sales call.
catalog tag: compliance_domain: "pii"
A customer-service agent pulls records from Salesforce to do its job — names, accounts, PII. What stops it leaking sensitive data on a tool call it shouldn't make — and could you prove to a regulator it didn't?
An agent calls a tool. Your policy engine says allow. The call goes through. None of that proves the engine itself wasn't tampered with, or that your audit log reflects what actually happened.
cMCP runs the policy where the agent can't reach it — every tool call your agent routes through the runtime is checked, enforced, and signed into a tamper-evident record. This quickstart lets you feel that in about ten minutes, on your laptop.
Five quick steps from install to a blocked leak
Everything leads to step four — the moment you watch your own policy stop something it shouldn't allow.
Install
One pip install — the runtime, that's it.
Write your policy
A few lines of Cedar — your rules, not ours.
Wire it in
Point one call at the runtime. Nothing else changes.
Watch it block
Fire a bad action. Watch the policy return 403.
Prove it
Get a signed, verifiable record of what happened.
The quickstart
Copy each block. We've filled in every file for you — nothing to hand-author, nothing to hash. Follow along top to bottom.
Install the runtime
Creates a project folder, an isolated Python environment, and installs the cmcp runtime — the enforcement engine. (The venv is why pip install won't get blocked on recent macOS.) No tool server needed — in Step 4 your policy blocks the call before it forwards, so salesforce.contacts is a placeholder for your real MCP server. Open source (MIT): github.com/agentrust-io/cmcp
mkdir -p cmcp-quickstart && cd cmcp-quickstart python3 -m venv venv && source venv/bin/activate pip install cmcp-runtime
Drop in the config & your policy
Paste each block into the same Terminal 1, top to bottom — you're already inside cmcp-quickstart from Step 1. The cat > … EOF wrapper writes each file for you, no editor needed. Your policy (block 2) is what decides what's allowed. Any *.cedar filename works; manifest.json and schema.cedarschema are the two names the runtime requires.
mkdir -p policies cat > cmcp-config.yaml << 'EOF' attestation: provider: auto enforcement_mode: enforcing # enforcing | advisory | silent policy_bundle_path: ./policies/ catalog_path: ./catalog.json listen_addr: "127.0.0.1:8443" # dev mode runs without a bearer token, so stay on loopback EOF
cat > policies/manifest.json << 'EOF'
{ "version": "0.1.0", "authored_at": "2026-06-05T00:00:00Z",
"author_identity": "[email protected]", "commit_sha": "quickstart-demo" }
EOF
cat > policies/agent.cedar << 'EOF'
// Cedar is default-deny: anything these rules don't permit is denied.
// Rule 1 — let the demo-agent workflow call tools at all
permit ( principal, action == cMCP::Action::"call_tool", resource )
when { context.workflow_id == "demo-agent" };
// Rule 2 — but never let it call a tool the catalog tags as PII.
// forbid always wins over permit in Cedar, so this is the rule
// that produces the 403 in Step 4.
forbid ( principal, action == cMCP::Action::"call_tool", resource )
when { context.compliance_domain == "pii" };
EOF
cat > policies/schema.cedarschema << 'EOF'
{"cMCP":{"entityTypes":{"Principal":{"memberOfTypes":[],"shape":{"type":"Record","attributes":{"session_id":{"type":"String","required":true},"workflow_id":{"type":"String","required":true}}}},"Resource":{"memberOfTypes":[],"shape":{"type":"Record","attributes":{"tool_name":{"type":"String","required":true}}}}},"actions":{"call_tool":{"appliesTo":{"principalTypes":["cMCP::Principal"],"resourceTypes":["cMCP::Resource"],"context":{"type":"Record","attributes":{"compliance_domain":{"type":"String","required":true},"session_max_sensitivity":{"type":"String","required":true},"workflow_id":{"type":"String","required":true}}}}}}}}
EOF
cat > catalog.json << 'EOF'
[
{
"tool_name": "salesforce.contacts",
"server": {
"display_name": "Salesforce Contacts MCP Server (mock)",
"url": "http://localhost:9001/mcp",
"tls_fingerprint": "SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"transport": "http-sse"
},
"approved_definition": {
"description": "Query Salesforce contacts by account name or contact ID.",
"input_schema": {
"type": "object",
"required": ["query"],
"properties": {
"query": {"type": "string", "description": "Account name or contact ID"},
"max_records": {"type": "integer", "default": 50}
}
},
"output_schema": {
"type": "object",
"properties": { "contacts": {"type": "array"}, "total_count": {"type": "integer"} }
}
},
"definition_hash": "sha256:b42ecf14612f23456b5b0794864a00288d4038ac444cedb87fc214cefee89e35",
"compliance_domain": "pii",
"requires_baa": false,
"sensitivity_level": "pii",
"added_at": "2026-06-05T00:00:00Z",
"approved_by": "[email protected]"
}
]
EOF
cmcp validate-config --config cmcp-config.yaml
Expect ✓ Config valid: cmcp-config.yaml. This checks the YAML only; the policy bundle and catalog are loaded when the runtime starts in Step 3. If you see an error instead, a block didn't paste cleanly — re-run that one.
Start the runtime
Run this in the same terminal where you just created the files (you're inside cmcp-quickstart). It keeps running and won't return a prompt — that's normal, and the startup notices are expected, not errors. Leave it open and open a second terminal for the next step.
CMCP_DEV_MODE=1 cmcp start --config cmcp-config.yaml
Fire a bad action — watch it get blocked
In a second terminal, run cd cmcp-quickstart && source venv/bin/activate first (so cmcp is available here too), then paste this. It's your support agent trying to pull a customer's Salesforce record. The runtime looks the tool up in your catalog, sees compliance_domain: "pii", and your Rule 2 forbids it. Watch it return a 403 before it ever reaches Salesforce. Not blocked? You're likely in advisory mode — check the config says enforcing.
curl -i -X POST http://localhost:8443/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {
"name": "salesforce.contacts",
"arguments": { "query": "Acme Corp" },
"_cmcp": { "session_id": "demo-session-001", "workflow_id": "demo-agent" }
}
}'
workflow_id is the only field the runtime reads out of _cmcp. The session id is a label for your own logs: the runtime mints its own session id, which is why Step 5 looks it up instead of assuming it.
Your policy stops a PII record from leaving on a tool call — before it reaches Salesforce, decided by the rule you wrote, enforced where the agent can't tamper with it. That's the barrier most teams can't cross today: shipping an agent you can actually prove is governed.
HTTP/1.1 403 Forbidden · "message": "Request denied by policy" · "error_code": "POLICY_DENY"
Walk away with proof
Closing the session mints a signed TRACE claim — a tamper-evident record of which tools ran and which policy decided each call. Verify it yourself; you don't have to trust us. See the full TRACE claim schema →
SID=$(curl -s "http://localhost:8443/audit/export?session_id=demo-session-001" \ | python3 -c "import sys,json; print(json.load(sys.stdin)['entries'][0]['session_id'])") curl -s -X POST "http://localhost:8443/sessions/$SID/close" | python3 -m json.tool > claim.json cmcp verify claim.json
Expected output in dev mode:
Every cryptographic check passes. The one FAIL is hardware attestation, which dev mode can't provide, and that is the honest answer: overall verification only reports verified when the claim is hardware-backed, so the CLI prints FAIL (partially_verified) and exits 1 here. Run the same runtime on a hardware TEE and that last check passes, the result becomes verified, and that is the version you hand a regulator.
The rule you just wrote is stateless: the tool is tagged PII, so it is always forbidden for this workflow. cMCP also tracks sensitivity across a session. Once a response has been inspected and found to contain PII, the session's session_max_sensitivity rises, and a rule like this starts to bite:
forbid ( principal, action == cMCP::Action::"call_tool", resource )
when { context.session_max_sensitivity == "pii" };
That one needs a session with history: sensitivity starts at public and only rises after the runtime inspects a real response, so you need an upstream tool server and at least one allowed call before it fires. The reference quickstart sets that up with a mock upstream: cMCP reference quickstart →
Stuck? Every block above is copy-paste — the file blocks write themselves via cat > … EOF, the rest are commands. Keep your first terminal (the runtime) running while you work in the second. Full walkthrough & troubleshooting: cMCP quickstart · debugging guide.
Tell the community how it went.
The people building cMCP hang out in Discord. Drop your result, get help, and tell us if it unblocks something you can't ship today.
Post your result in #general and answer one question — did it unblock something you can't ship today?
Join the Discord →