The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Chp MCP listing page.
@cubiczan/chp-mcpOne-command MCP install for CHP Profile B spend / capital gates and tool-approval receipts (an allowlist is not authorization), plus a structured deny ledger and receipt-gated finance tools.
Wraps @cubiczan/chp so Cursor,
Claude Code, or any MCP client can call evaluate_spend_gate without vendoring
protocol code. Engine digests match the normative golden vectors
(Profile B 30/30).
For AGENTS.md + skills + Profile A decision_gate / decision_adversary, use
agent-conductor instead.
| Tool | Maps to | Purpose |
|---|---|---|
evaluate_spend_gate | evaluateGate | LOCKED / HITL_REQUIRED / BLOCKED + claims + content hash. BLOCKED is also a ledgered policy_deny. |
approve_spend | approveHuman | Human lock when HITL_REQUIRED (cannot override hard fails). Optional tool + bound_args mint a signed receipt. |
evaluate_tool_approval | evaluateToolApproval | Allowlist is a pre-filter; host-bound fields merge into args_hash; a receipt is still required |
issue_approval_receipt | issueApprovalReceipt | Human allow/deny → HMAC-signed receipt + decision log |
authorize_tool_call | authorizeToolCall | Consume a receipt; deny on drift, host-bound override, expiry, replay, or a bad MAC |
request_authorization | runtime | Mint a receipt bound to a scoped reference tool, or return HITL / structured deny |
place_equity_order | reference | Synthetic equity order — scope trading:equities:place, receipt required |
wire_treasury_transfer | reference | Synthetic treasury wire — scope treasury:wire, always HITL |
rebalance_portfolio | reference | Synthetic rebalance — scope portfolio:rebalance |
inspect_audit_ledger | ledger | Trailing CHP-chained deny / authorize / execute entries |
chp_content_hash | contentHash | Float-aware canonical SHA-256 |
chp_version | — | Server + protocol versions + deny reason codes + receipt schema |
Managed MCP allowlists (Cursor mcpServers, Claude Desktop, Claude Code)
only answer “is this tool name installed?”. They do not bind tenant,
arguments, risk, or a human decision. This server treats that gap as a
hard deny unless a signed approval receipt still matches the call
that is about to run.
Receipts are HMAC-SHA256 over CHP canonical JSON
(the same payload discipline as Profile B contentHash / audit-ledger
sig). The MAC covers:
| Field | Role |
|---|---|
actor | Human who allowed or denied |
tool | Concrete tool name (no *) |
resource | Tenant / resource binding (no *) |
args_hash | contentHash(host ∪ model arguments, { floatAware: true }) |
policy_version | Policy the human saw |
risk | Policy risk for that tool |
issued_at / expiry | Lifetime |
decision | allow or deny |
nonce | Single-use; replay denies |
signature | HMAC-SHA256 hex |
Set CHP_RECEIPT_KEY (or AUDIT_LEDGER_KEY) in the MCP server env.
Without it the process falls back to a documented insecure default —
fine for the local cookbook, not for production.
Example policy: examples/tool-approval-policy.json.
stripe.create_charge is on the allowlist and still cannot run
without a receipt bound to acct_live_acme and the exact charge args.
Host-injected tenant/index bindings use
examples/host-injected-policy.json
(see the host-injected args cookbook below).
Claude/Cursor has stripe.create_charge enabled. That is not a grant.
Result: RECEIPT_REQUIRED, deny_code: "allowlist_is_not_authorization".
Calling authorize_tool_call with the same payload and no receipt
returns DENIED / allowlist_is_not_authorization.
The response includes receipt (take the whole object) and
decision_log (actor, decision, args hash, nonce). Pass that receipt
into authorize_tool_call with the same call. Result: AUTHORIZED.
Issue with "decision": "deny". The decision is logged. Authorizing
with that receipt returns DENIED / human_denied. A deny receipt
cannot be flipped to allow by editing decision — the MAC breaks.
Approve { "amount": 2500, ... }, then authorize with
{ "amount": 2500000, ... }. Result: DENIED / changed_arguments.
Key order does not matter; the hash is CHP canonical. The original
receipt remains valid for the args that were approved (until expiry or
a successful consume).
Issue with ttl_seconds: 30. After the expiry instant,
authorize_tool_call returns DENIED / expired_receipt. The nonce is
consumed so a clock rewind cannot resurrect it.
A successful AUTHORIZED consume burns the nonce. Presenting the same
receipt again returns DENIED / replayed_receipt.
These never produce a usable allow receipt:
resource: "*", any, all, or an empty stringargumentsversionFail-closed: deny_on_ambiguity cannot be turned off.
_metaSemantic Kernel and other hosts need to pass index, key, and tenant
without letting the model choose them
(SO-style routing).
Putting those fields on the tool schema so the LLM can “decide” is the
bug. An MCP allowlist does not fix it: the tool name can stay
allowlisted while the model swaps index_name to another tenant.
The host (or a gateway in front of this server) injects bound fields. This package hashes host ∪ model arguments into the receipt and denies when the model overrides a host-bound field. The allowlist is still only a pre-filter.
_meta.cubiczan (no hard dependency)@cubiczan/governed-mcp-gateway
already injects identity on every tools/call and SSE frame:
This server does not import that package. It reads the same
envelope. Hosts MAY add host_bound next to principal. A trusted
gateway should overwrite _meta.cubiczan so the model cannot self-attest.
Library callers can also pass host_bound on the proposed call
(explicit keys overlay _meta). Policy
examples/host-injected-policy.json
declares host_bound_fields so index_name and tenant_id must be
host-injected and concrete. If tenant_id is declared and omitted,
_meta.cubiczan.principal.orgId may fill it.
The model chose query / top_k. The host chose the index.
Result: RECEIPT_REQUIRED, deny_code: "allowlist_is_not_authorization".
args_hash is contentHash of
{ query, top_k, tenant_id, index_name } — not the model object alone.
Same host _meta, but the model adds "index_name": "other-index".
evaluate_tool_approval, issue_approval_receipt (decision: "allow"),
and authorize_tool_call all return DENIED /
host_bound_override. Matching the host value is not an override.
Issue a receipt for the host-injected call. Authorize with the same
arguments and _meta. Result: AUTHORIZED. Change query after
approve → changed_arguments. Change index_name in model args →
host_bound_override. Omit declared host fields → ambiguous.
Do the routing in the host, not the model: disable auto-invoke, then
inject index/tenant (or put a gateway in front that writes
_meta.cubiczan.host_bound) before evaluate_tool_approval /
authorize_tool_call. Secrets such as API keys belong in the host or
the gateway vault — not in the tool schema the LLM sees.
MCP denials are usually a bare error string. That string is gone when the client disconnects. This server treats a refuse as a structured event that must hit a CHP-signed ledger before the caller sees it.
Finance tools (place_equity_order, wire_treasury_transfer,
rebalance_portfolio) are synthetic — no live venue or bank rail — and
use a separate kind: "authorization" receipt bound to tool, scope, and
args hash. That is not the same object as a chp.tool_approval_receipt.
| Code | When |
|---|---|
policy_deny | Hard CHP rule failed (max_notional, daily cap, …) |
expired | Receipt expires_at is in the past |
replay | Receipt already consumed by a successful execute |
args_changed | Tool, scope, or args hash no longer matches the receipt |
missing_receipt | No receipt, or the content hash does not verify |
ambiguous_policy | Unknown tool, scope mismatch, or incomplete policy |
Signing is the existing Profile B primitives: contentHash on the
receipt / ledger payload, chainHash between ledger rows. Set
CHP_AUDIT_LEDGER to a JSONL path (default ./data/chp-audit.jsonl),
or :memory: for tests.
Under the HITL threshold the gate auto-locks and mints a receipt. At or
above it, pass approver (or call approve_spend with tool +
bound_args).
Treasury wires use hitl_threshold: 0. A request without approver
returns HITL_REQUIRED and no receipt — that is the approval gate,
not a weather-API demo.
receipt is optional on the wire so a missing token is a logged
missing_receipt deny, not a schema 400 that never hits the ledger.
Change notional or quantity after approve → args_changed, and the
ledger has the deny. Call again with the same receipt → replay.
Call with no receipt → missing_receipt. All three are durable.
Each row carries content_hash and sig = chainHash(prev_sig, { seq, ts, event, content_hash }).
chain.ok is false if anyone rewrote history.
This Cubiczan mirror may omit GitHub Actions; run the suite locally.
npm test builds, then runs node --test dist/*.test.js (approval
receipts + host-injected bindings) and
node --import tsx --test test/**/*.test.ts (deny ledger). Invariants
covered: an unlogged deny is impossible (ledger failure throws instead
of returning a deny object); changed args after approve deny; a receipt
is required for every gated reference tool; allowlist is not
authorization; host-bound tenant/index cannot be overridden by the
model; receipt args_hash covers host ∪ model args.
| Package / repo | Role |
|---|---|
@cubiczan/chp | Profile B library (this server’s dependency) |
consensus-hardening-protocol | Profile A + normative spec |
@cubiczan/agent-conductor | Full MCP: contracts, skills, Profile A gates |
@cubiczan/governed-mcp-gateway | HTTP MCP control plane |
@cubiczan/codesentinel-mcp | Codebase health MCP |
cubiczan-resilience / @cubiczan/resilience | Shared retry / timeout / audit primitives |
MIT.