{
  "markdown": "# `@cubiczan/chp-mcp`\n\n[![icohangar-ops/cubiczan-chp-mcp MCP server](https://glama.ai/mcp/servers/icohangar-ops/cubiczan-chp-mcp/badges/score.svg)](https://glama.ai/mcp/servers/icohangar-ops/cubiczan-chp-mcp)\n\n\nOne-command MCP install for **CHP Profile B** spend / capital gates and\n**tool-approval receipts** (an allowlist is not authorization), plus a\n**structured deny ledger** and receipt-gated finance tools.\n\n[![MCP Registry](https://img.shields.io/badge/MCP_Registry-io.github.icohangar--ops%2Fchp--mcp-00C4B4)](https://registry.modelcontextprotocol.io)\n[![npm](https://img.shields.io/npm/v/@cubiczan/chp-mcp)](https://www.npmjs.com/package/@cubiczan/chp-mcp)\n[![Conformance](https://img.shields.io/badge/CHP_Profile_B-30%2F30-brightgreen)](https://github.com/icohangar-ops/cubiczan-chp)\n\nWraps [`@cubiczan/chp`](https://www.npmjs.com/package/@cubiczan/chp) so Cursor,\nClaude Code, or any MCP client can call `evaluate_spend_gate` without vendoring\nprotocol code. Engine digests match the normative golden vectors\n(**Profile B 30/30**).\n\n## How the pieces fit\n\n```text\nMCP client (Cursor / Claude / …)\n        │  tools/call\n        ▼\n┌───────────────────────────┐\n│  MCP server (transport)   │  ← you are here (@cubiczan/chp-mcp)\n│  evaluate_spend_gate      │\n│  approve_spend            │\n│  evaluate_tool_approval   │  allowlist ≠ authorization\n│  issue_approval_receipt   │\n│  authorize_tool_call      │\n│  request_authorization    │  finance-tool receipt / HITL / deny\n│  place_equity_order       │  scoped + receipt-gated (synthetic)\n│  wire_treasury_transfer   │\n│  rebalance_portfolio      │\n│  inspect_audit_ledger     │  CHP-signed deny / authorize / execute\n│  chp_content_hash         │\n└─────────────┬─────────────┘\n              │ depends on\n              ▼\n┌───────────────────────────┐\n│  Published CHP packages   │\n│  npm:  @cubiczan/chp                 (Profile B)\n│  PyPI: consensus-hardening-protocol  (Profile A)\n└───────────────────────────┘\n```\n\nFor AGENTS.md + skills + Profile A `decision_gate` / `decision_adversary`, use\n[agent-conductor](https://github.com/icohangar-ops/agent-conductor) instead.\n\n## Install\n\n```bash\nnpm install -g @cubiczan/chp-mcp\n# or one-shot\nnpx -y @cubiczan/chp-mcp\n```\n\n### Cursor / Claude Desktop\n\n```json\n{\n  \"mcpServers\": {\n    \"chp\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@cubiczan/chp-mcp\"]\n    }\n  }\n}\n```\n\n### Claude Code\n\n```bash\nclaude mcp add chp -- npx -y @cubiczan/chp-mcp\n```\n\n## Tools\n\n| Tool | Maps to | Purpose |\n|------|---------|---------|\n| `evaluate_spend_gate` | `evaluateGate` | LOCKED / HITL_REQUIRED / BLOCKED + claims + content hash. `BLOCKED` is also a ledgered `policy_deny`. |\n| `approve_spend` | `approveHuman` | Human lock when HITL_REQUIRED (cannot override hard fails). Optional `tool` + `bound_args` mint a signed receipt. |\n| `evaluate_tool_approval` | `evaluateToolApproval` | Allowlist is a pre-filter; host-bound fields merge into `args_hash`; a receipt is still required |\n| `issue_approval_receipt` | `issueApprovalReceipt` | Human allow/deny → HMAC-signed receipt + decision log |\n| `authorize_tool_call` | `authorizeToolCall` | Consume a receipt; deny on drift, host-bound override, expiry, replay, or a bad MAC |\n| `request_authorization` | runtime | Mint a receipt bound to a scoped reference tool, or return HITL / structured deny |\n| `place_equity_order` | reference | Synthetic equity order — scope `trading:equities:place`, receipt required |\n| `wire_treasury_transfer` | reference | Synthetic treasury wire — scope `treasury:wire`, always HITL |\n| `rebalance_portfolio` | reference | Synthetic rebalance — scope `portfolio:rebalance` |\n| `inspect_audit_ledger` | ledger | Trailing CHP-chained deny / authorize / execute entries |\n| `chp_content_hash` | `contentHash` | Float-aware canonical SHA-256 |\n| `chp_version` | — | Server + protocol versions + deny reason codes + receipt schema |\n\n### Example — evaluate a spend\n\n```jsonc\n// tools/call evaluate_spend_gate\n{\n  \"action\": { \"action\": \"LONG\", \"asset\": \"ETH\", \"notional\": 300, \"confidence\": 0.9 },\n  \"policy\": {\n    \"max_notional\": 500,\n    \"daily_cap\": 2500,\n    \"hitl_threshold\": 250,\n    \"min_confidence\": 0.55,\n    \"allowed_actions\": [\"LONG\", \"SHORT\"]\n  }\n}\n```\n\n## Cookbook — Claude / Cursor tool approval\n\nManaged MCP allowlists (Cursor `mcpServers`, Claude Desktop, Claude Code)\nonly answer *“is this tool name installed?”*. They do not bind tenant,\narguments, risk, or a human decision. This server treats that gap as a\nhard deny unless a signed **approval receipt** still matches the call\nthat is about to run.\n\nReceipts are HMAC-SHA256 over [CHP canonical JSON](https://www.npmjs.com/package/@cubiczan/chp)\n(the same payload discipline as Profile B `contentHash` / audit-ledger\n`sig`). The MAC covers:\n\n| Field | Role |\n|-------|------|\n| `actor` | Human who allowed or denied |\n| `tool` | Concrete tool name (no `*`) |\n| `resource` | Tenant / resource binding (no `*`) |\n| `args_hash` | `contentHash(host ∪ model arguments, { floatAware: true })` |\n| `policy_version` | Policy the human saw |\n| `risk` | Policy risk for that tool |\n| `issued_at` / `expiry` | Lifetime |\n| `decision` | `allow` or `deny` |\n| `nonce` | Single-use; replay denies |\n| `signature` | HMAC-SHA256 hex |\n\nSet `CHP_RECEIPT_KEY` (or `AUDIT_LEDGER_KEY`) in the MCP server env.\nWithout it the process falls back to a documented insecure default —\nfine for the local cookbook, not for production.\n\nExample policy: [`examples/tool-approval-policy.json`](examples/tool-approval-policy.json).\n`stripe.create_charge` is **on the allowlist** and still cannot run\nwithout a receipt bound to `acct_live_acme` and the exact charge args.\nHost-injected tenant/index bindings use\n[`examples/host-injected-policy.json`](examples/host-injected-policy.json)\n(see the host-injected args cookbook below).\n\n```json\n{\n  \"mcpServers\": {\n    \"chp\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@cubiczan/chp-mcp\"],\n      \"env\": { \"CHP_RECEIPT_KEY\": \"replace-me\" }\n    }\n  }\n}\n```\n\n### 1. Allowlist alone — denied\n\nClaude/Cursor has `stripe.create_charge` enabled. That is not a grant.\n\n```jsonc\n// tools/call evaluate_tool_approval\n{\n  \"call\": {\n    \"tool\": \"stripe.create_charge\",\n    \"resource\": \"acct_live_acme\",\n    \"arguments\": { \"amount\": 2500, \"currency\": \"usd\", \"customer\": \"cus_123\" }\n  },\n  \"policy\": { \"$ref\": \"examples/tool-approval-policy.json\" }\n}\n```\n\nResult: `RECEIPT_REQUIRED`, `deny_code: \"allowlist_is_not_authorization\"`.\n\nCalling `authorize_tool_call` with the same payload and **no receipt**\nreturns `DENIED` / `allowlist_is_not_authorization`.\n\n### 2. Human allow — then authorize\n\n```jsonc\n// tools/call issue_approval_receipt\n{\n  \"actor\": \"cfo@acme.example\",\n  \"decision\": \"allow\",\n  \"reason\": \"invoice INV-104 matches amount\",\n  \"ttl_seconds\": 120,\n  \"call\": { /* same as above */ },\n  \"policy\": { /* same as above */ }\n}\n```\n\nThe response includes `receipt` (take the whole object) and\n`decision_log` (actor, decision, args hash, nonce). Pass that receipt\ninto `authorize_tool_call` with the **same** call. Result: `AUTHORIZED`.\n\n### 3. Human deny\n\nIssue with `\"decision\": \"deny\"`. The decision is logged. Authorizing\nwith that receipt returns `DENIED` / `human_denied`. A deny receipt\ncannot be flipped to allow by editing `decision` — the MAC breaks.\n\n### 4. Changed arguments after approval — denied\n\nApprove `{ \"amount\": 2500, ... }`, then authorize with\n`{ \"amount\": 2500000, ... }`. Result: `DENIED` / `changed_arguments`.\nKey order does not matter; the hash is CHP canonical. The original\nreceipt remains valid for the args that were approved (until expiry or\na successful consume).\n\n### 5. Expired receipt — denied\n\nIssue with `ttl_seconds: 30`. After the expiry instant,\n`authorize_tool_call` returns `DENIED` / `expired_receipt`. The nonce is\nconsumed so a clock rewind cannot resurrect it.\n\n### 6. Replayed receipt — denied\n\nA successful `AUTHORIZED` consume burns the nonce. Presenting the same\nreceipt again returns `DENIED` / `replayed_receipt`.\n\n### 7. Ambiguity — denied\n\nThese never produce a usable allow receipt:\n\n- `resource: \"*\"`, `any`, `all`, or an empty string\n- missing `arguments`\n- policy without a concrete `version`\n- actor / tool wildcards\n- extra keys on a receipt (strict parse)\n\nFail-closed: `deny_on_ambiguity` cannot be turned off.\n\n## Cookbook — host-injected args + gateway `_meta`\n\nSemantic Kernel and other hosts need to pass **index, key, and tenant**\nwithout letting the model choose them\n([SO-style routing](https://stackoverflow.com/questions/79748920/how-to-pass-dynamic-parameters-eg-index-name-key-from-semantic-kernel-to-mcp)).\nPutting those fields on the tool schema so the LLM can “decide” is the\nbug. An MCP allowlist does not fix it: the tool name can stay\nallowlisted while the model swaps `index_name` to another tenant.\n\nThe host (or a gateway in front of this server) injects bound fields.\nThis package hashes **host ∪ model** arguments into the receipt and\ndenies when the model overrides a host-bound field. The allowlist is\nstill only a pre-filter.\n\n### Contract — `_meta.cubiczan` (no hard dependency)\n\n[`@cubiczan/governed-mcp-gateway`](https://www.npmjs.com/package/@cubiczan/governed-mcp-gateway)\nalready injects identity on every `tools/call` and SSE frame:\n\n```json\n{\n  \"_meta\": {\n    \"cubiczan\": {\n      \"principal\": {\n        \"id\": \"agt_search\",\n        \"kind\": \"agent\",\n        \"orgId\": \"org_acme\",\n        \"displayName\": \"Search Runner\"\n      }\n    }\n  }\n}\n```\n\nThis server does **not** import that package. It reads the same\nenvelope. Hosts MAY add `host_bound` next to `principal`. A trusted\ngateway should overwrite `_meta.cubiczan` so the model cannot self-attest.\n\n```json\n{\n  \"_meta\": {\n    \"cubiczan\": {\n      \"principal\": { \"id\": \"agt_search\", \"kind\": \"agent\", \"orgId\": \"org_acme\" },\n      \"host_bound\": { \"tenant_id\": \"acme\", \"index_name\": \"prod-docs\" }\n    }\n  }\n}\n```\n\nLibrary callers can also pass `host_bound` on the proposed call\n(explicit keys overlay `_meta`). Policy\n[`examples/host-injected-policy.json`](examples/host-injected-policy.json)\ndeclares `host_bound_fields` so `index_name` and `tenant_id` must be\nhost-injected and concrete. If `tenant_id` is declared and omitted,\n`_meta.cubiczan.principal.orgId` may fill it.\n\n```text\nmodel args ──┐\n             ├─ override check ─→ deny host_bound_override\nhost_bound ──┘         │\n                       ▼\n              merged args → args_hash → receipt MAC\n                       │\nallowlist ──── pre-filter only (never a grant)\n```\n\n### 1. Host injects index + tenant — allowlist still denied\n\nThe model chose `query` / `top_k`. The host chose the index.\n\n```jsonc\n// tools/call evaluate_tool_approval\n{\n  \"call\": {\n    \"tool\": \"search.azure_ai\",\n    \"resource\": \"tenant:acme\",\n    \"arguments\": { \"query\": \"Q3 revenue\", \"top_k\": 5 },\n    \"_meta\": {\n      \"cubiczan\": {\n        \"principal\": { \"id\": \"agt_search\", \"kind\": \"agent\", \"orgId\": \"org_acme\" },\n        \"host_bound\": { \"tenant_id\": \"acme\", \"index_name\": \"prod-docs\" }\n      }\n    }\n  },\n  \"policy\": { \"$ref\": \"examples/host-injected-policy.json\" }\n}\n```\n\nResult: `RECEIPT_REQUIRED`, `deny_code: \"allowlist_is_not_authorization\"`.\n`args_hash` is `contentHash` of\n`{ query, top_k, tenant_id, index_name }` — not the model object alone.\n\n### 2. Model changes a host-bound field — denied\n\nSame host `_meta`, but the model adds `\"index_name\": \"other-index\"`.\n\n```jsonc\n\"arguments\": { \"query\": \"Q3 revenue\", \"top_k\": 5, \"index_name\": \"other-index\" }\n```\n\n`evaluate_tool_approval`, `issue_approval_receipt` (`decision: \"allow\"`),\nand `authorize_tool_call` all return `DENIED` /\n`host_bound_override`. Matching the host value is not an override.\n\n### 3. Human allow — then authorize the merged args\n\nIssue a receipt for the host-injected call. Authorize with the **same**\n`arguments` and `_meta`. Result: `AUTHORIZED`. Change `query` after\napprove → `changed_arguments`. Change `index_name` in model args →\n`host_bound_override`. Omit declared host fields → `ambiguous`.\n\n### 4. Semantic Kernel / host wiring\n\nDo the routing in the host, not the model: disable auto-invoke, then\ninject index/tenant (or put a gateway in front that writes\n`_meta.cubiczan.host_bound`) before `evaluate_tool_approval` /\n`authorize_tool_call`. Secrets such as API keys belong in the host or\nthe gateway vault — not in the tool schema the LLM sees.\n\n## Cookbook — deny telemetry and receipts\n\nMCP denials are usually a bare error string. That string is gone when the\nclient disconnects. This server treats a refuse as a **structured event**\nthat must hit a CHP-signed ledger *before* the caller sees it.\n\nFinance tools (`place_equity_order`, `wire_treasury_transfer`,\n`rebalance_portfolio`) are synthetic — no live venue or bank rail — and\nuse a separate `kind: \"authorization\"` receipt bound to tool, scope, and\nargs hash. That is not the same object as a `chp.tool_approval_receipt`.\n\n### Reason codes\n\n| Code | When |\n|------|------|\n| `policy_deny` | Hard CHP rule failed (`max_notional`, daily cap, …) |\n| `expired` | Receipt `expires_at` is in the past |\n| `replay` | Receipt already consumed by a successful execute |\n| `args_changed` | Tool, scope, or args hash no longer matches the receipt |\n| `missing_receipt` | No receipt, or the content hash does not verify |\n| `ambiguous_policy` | Unknown tool, scope mismatch, or incomplete policy |\n\nSigning is the existing Profile B primitives: `contentHash` on the\nreceipt / ledger payload, `chainHash` between ledger rows. Set\n`CHP_AUDIT_LEDGER` to a JSONL path (default `./data/chp-audit.jsonl`),\nor `:memory:` for tests.\n\n### 1. Request a bound receipt\n\nUnder the HITL threshold the gate auto-locks and mints a receipt. At or\nabove it, pass `approver` (or call `approve_spend` with `tool` +\n`bound_args`).\n\n```jsonc\n// tools/call request_authorization\n{\n  \"tool\": \"place_equity_order\",\n  \"args\": {\n    \"symbol\": \"AAPL\",\n    \"side\": \"BUY\",\n    \"quantity\": 10,\n    \"notional\": 300,\n    \"confidence\": 0.9\n  },\n  \"approver\": \"cfo@example.com\"\n}\n```\n\nTreasury wires use `hitl_threshold: 0`. A request without `approver`\nreturns `HITL_REQUIRED` and **no** receipt — that is the approval gate,\nnot a weather-API demo.\n\n### 2. Execute only with that receipt\n\n`receipt` is optional on the wire so a missing token is a logged\n`missing_receipt` deny, not a schema 400 that never hits the ledger.\n\n```jsonc\n// tools/call place_equity_order\n{\n  \"symbol\": \"AAPL\",\n  \"side\": \"BUY\",\n  \"quantity\": 10,\n  \"notional\": 300,\n  \"confidence\": 0.9,\n  \"receipt\": { \"kind\": \"authorization\", \"receipt_id\": \"…\", \"content_hash\": \"…\" }\n}\n```\n\nChange `notional` or `quantity` after approve → `args_changed`, and the\nledger has the deny. Call again with the same receipt → `replay`.\nCall with no receipt → `missing_receipt`. All three are durable.\n\n### 3. Inspect the chain\n\n```jsonc\n// tools/call inspect_audit_ledger\n{ \"limit\": 20 }\n```\n\nEach row carries `content_hash` and `sig = chainHash(prev_sig, { seq, ts, event, content_hash })`.\n`chain.ok` is false if anyone rewrote history.\n\n### Tests\n\n```bash\nnpm test\n```\n\nThis Cubiczan mirror may omit GitHub Actions; run the suite locally.\n`npm test` builds, then runs `node --test dist/*.test.js` (approval\nreceipts + host-injected bindings) and\n`node --import tsx --test test/**/*.test.ts` (deny ledger). Invariants\ncovered: an unlogged deny is impossible (ledger failure throws instead\nof returning a deny object); changed args after approve deny; a receipt\nis required for every gated reference tool; allowlist is not\nauthorization; host-bound tenant/index cannot be overridden by the\nmodel; receipt `args_hash` covers host ∪ model args.\n\n## Related\n\n| Package / repo | Role |\n|----------------|------|\n| [`@cubiczan/chp`](https://www.npmjs.com/package/@cubiczan/chp) | Profile B library (this server’s dependency) |\n| [`consensus-hardening-protocol`](https://pypi.org/project/consensus-hardening-protocol/) | Profile A + normative spec |\n| [`@cubiczan/agent-conductor`](https://www.npmjs.com/package/@cubiczan/agent-conductor) | Full MCP: contracts, skills, Profile A gates |\n| [`@cubiczan/governed-mcp-gateway`](https://www.npmjs.com/package/@cubiczan/governed-mcp-gateway) | HTTP MCP control plane |\n| [`@cubiczan/codesentinel-mcp`](https://www.npmjs.com/package/@cubiczan/codesentinel-mcp) | Codebase health MCP |\n| [`cubiczan-resilience`](https://pypi.org/project/cubiczan-resilience/) / [`@cubiczan/resilience`](https://www.npmjs.com/package/@cubiczan/resilience) | Shared retry / timeout / audit primitives |\n\n## Licence\n\nMIT.\n",
  "bytes": 16541,
  "sha": "a49d0fc134e356e5497aa1722e1cf51258a2b9c4c5261c0c30161793943e87aa",
  "repo_slug": "icohangar-ops/cubiczan-chp-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_icohangar_ops_chp_mcp_b2180ed2/readme"
}