@0xgks/mandate-mcp
v0.1.0
Published
MCP server for proof-gated MANDATE agent execution — a thin stdio wrapper around @0xgks/mandate-sdk for MoonPay Agent, Claude, Codex, and other MCP-compatible clients.
Readme
@0xgks/mandate-mcp
An MCP (Model Context Protocol) server that exposes MANDATE — a proof-gated, policy-constrained order execution protocol — to MoonPay Agent, Claude, Codex, or any other MCP-compatible AI client.
This package is a thin stdio wrapper around
@0xgks/mandate-sdk. It
does not reimplement any prover, commitment, Merkle-tree, or submission
logic — every order that goes through it is proven with a real Noir/Barretenberg
zero-knowledge proof and submitted through the real MANDATE contracts and
sequencer, exactly as @0xgks/mandate-sdk's MandateClient.proveAndSubmit
does it.
⚠️ MANDATE is unaudited research software. Do not use it, or this MCP server, with real funds or real private keys. The demo/test keys referenced anywhere in this repository are Anvil's public, well-known test keys — never use them (or this software) outside a local, throwaway chain.
Architecture
MoonPay Agent / Claude / Codex
↓ (MCP over stdio)
@0xgks/mandate-mcp (this package — tool surface + env config only)
↓ (proveAndSubmit)
@0xgks/mandate-sdk (prover wrapper, commitments, Merkle, submission)
↓
Noir prover (nargo + bb) + MANDATE sequencer + MANDATE smart contractsMoonPay is the agent interface; MANDATE is the execution-policy layer.
This server does not talk to MoonPay's trading APIs at all — "MoonPay Agent"
here means the MCP-compatible agent runtime that will have this server
registered as one of its tools. Whatever gives that agent read-only market
data, quotes, or balances (MoonPay's own read-only tools, if you wire them
up) is a separate concern from whether an order is allowed to execute.
Execution only ever happens through this server's mandate_submit_order
tool, which is proof-gated: see the security warning below.
What this server does NOT do
- It does not expose any MoonPay swap, transfer, bridge, buy/sell, or
wallet-signing tool. There is no way to move funds through this server
except by proving compliance and going through
mandate_submit_order→MandateClient.proveAndSubmit. - It does not implement a
previewOrder/dry-run tool.@0xgks/mandate-sdkdoes not expose one, and this integration does not invent one — a "preview" that isn't backed by the real prover would be misleading about what authorizes execution. - It does not modify the Noir circuit, the contracts, the sequencer, or
@0xgks/mandate-sdk. It only calls the SDK's public API and a small number of already-public, read-only contract views / sequencer endpoints.
⚠️ Security warning: do not also expose unrestricted MoonPay execution tools
If the same agent session also has access to unrestricted MoonPay
transaction tools (swap, transfer, bridge, arbitrary signing), the agent can
simply bypass MANDATE entirely by calling those tools directly instead of
mandate_submit_order. MANDATE's proof-gating only has teeth if it is the
only execution path available to the agent.
For a meaningful demo/deployment of the security model, an agent using this server should be given:
- ✅ MANDATE's tools from this package (
mandate_get_portfolio,mandate_get_epoch,mandate_submit_order) - ✅ read-only MoonPay tools if useful (balances, prices, token metadata, quotes)
- ❌ not MoonPay's (or any other) unrestricted execution/signing tools
Prerequisites
This server calls out to the same toolchain the rest of the MANDATE repo needs — it does not bundle or replace any of it:
- Node.js ≥ 20
nargo(matching the version used bycircuits/policy_check) reachable onPATH, or viaMANDATE_NARGO_PATHbb(Barretenberg), reachable onPATH, or viaMANDATE_BB_PATH- A running MANDATE stack to talk to: an EVM RPC endpoint with the
MandateRegistry/BatchAuctioncontracts deployed, and a running MANDATE sequencer — see the repo root README /demo/run.shfor how to stand up a local Anvil-based stack - The compiled circuit artifacts (
target/policy_check.json,target/vk) for whatever circuit directoryMANDATE_CIRCUIT_PATHpoints at
Installation
Inside this monorepo (workspace-local development):
npm install
npm run build -w @0xgks/mandate-mcpAs a published package (once published):
npm install -g @0xgks/mandate-mcp
# or run without installing:
npx @0xgks/mandate-mcpConfiguration
All configuration is read from environment variables only — never from MCP tool arguments. This is intentional: an MCP client can ask this server to submit an order, but it can never redirect the server at a different RPC endpoint, a different sequencer, a different signer, or a different circuit.
See .env.example for a placeholder-only template (never
commit a real .env — it's git-ignored, along with .env.*, everywhere
except this example file).
Required
| Variable | Description |
|---|---|
| MANDATE_AGENT_ID | This agent's identity in the registry. A 0x-prefixed bytes32 hex value, or a plain decimal integer (padded automatically). |
| MANDATE_SESSION_KEY | The session EOA's private key (0x-prefixed, 32 bytes). Never logged. The session key has zero authority over funds in MANDATE's design — only over proof-gated order submission. |
| MANDATE_AUCTION_ADDRESS | BatchAuction contract address. |
| MANDATE_REGISTRY_ADDRESS | MandateRegistry contract address. |
| MANDATE_CIRCUIT_PATH | Absolute path to the Noir circuit project directory (containing Nargo.toml and a compiled target/) used for proving. |
Also required: the plaintext mandate (policy) parameters
@0xgks/mandate-sdk's MandateClient requires the plaintext mandate
parameters — the opening of the on-chain policy commitment — to prove
against. There is no way to recover this from the chain (only the one-way
Poseidon2 commitment is stored on-chain), so it must be supplied here. These
five variables must match exactly what is registered on-chain for
MANDATE_AGENT_ID (MandateClient checks this and refuses to prove
otherwise — see PolicyMismatchError):
| Variable | Description |
|---|---|
| MANDATE_WHITELIST_ROOT | Merkle root of the market whitelist (decimal or 0x-hex). |
| MANDATE_MAX_ORDER_NOTIONAL | Max per-order notional (size * limitPrice). |
| MANDATE_MAX_POSITION | Max post-fill absolute position. |
| MANDATE_MAX_DAILY_LOSS | Max daily loss before the mandate rejects further risk-increasing orders. |
| MANDATE_POLICY_SALT | The policy commitment's salt. |
These five variables are not part of the original brief's short env var list — they were added after inspecting
@0xgks/mandate-sdk's actualMandateClientConfigtype (packages/mandate-sdk/src/types.ts), which declarespolicy: PolicyParamsas a required (non-optional) field. See the top ofsrc/config.tsfor the same note in code.
Optional (with defaults)
| Variable | Default | Description |
|---|---|---|
| MANDATE_RPC_URL | http://127.0.0.1:8545 | EVM JSON-RPC endpoint. |
| MANDATE_SEQUENCER_URL | http://127.0.0.1:8787 | MANDATE sequencer base URL. |
| MANDATE_NARGO_PATH | $NARGO_BIN or nargo on PATH | Override the nargo binary. |
| MANDATE_BB_PATH | $BB_BIN or bb on PATH | Override the bb binary. |
| MANDATE_MARKET_MAP | {"ETH/USDC":"1","ETH":"1","WBTC/USDC":"2","WBTC":"2"} | JSON object mapping human-readable market symbols (as used in tool calls, e.g. "ETH/USDC") to the circuit's numeric market id. Merged over the default map. A plain numeric string (e.g. "1") always passes through unchanged. |
Local development
# run directly against source with tsx (no build step)
MANDATE_AGENT_ID=0x... MANDATE_SESSION_KEY=0x... ... npm run dev -w @0xgks/mandate-mcp
# or build once, then run the compiled binary
npm run build -w @0xgks/mandate-mcp
MANDATE_AGENT_ID=0x... MANDATE_SESSION_KEY=0x... ... node packages/mandate-mcp/dist/index.jsThe server communicates exclusively over stdio — it prints nothing to stdout except MCP protocol frames. All diagnostics (startup errors, proving failures, unexpected errors) go to stderr.
MCP Inspector
@modelcontextprotocol/inspector
is the standard way to interactively exercise an MCP server over stdio:
cd packages/mandate-mcp
npx @modelcontextprotocol/inspector node dist/index.jsThis opens a local web UI where you can browse the exposed tools and call
them by hand. It also has a scriptable --cli mode, e.g.:
npx @modelcontextprotocol/inspector --cli node dist/index.js --method tools/list(Note: the Inspector CLI's --tool-arg coerces numeric-looking values to
JSON numbers, which will fail this server's schemas — amount/limitPrice
are intentionally strings. Use the interactive web UI, or a raw JSON-RPC/stdio
client, to pass them correctly quoted as strings.)
MoonPay Agent configuration
Register this server as an MCP tool provider. Restart the MCP client fully after any configuration change — most clients only read this file at startup.
Development (local build)
{
"mcpServers": {
"mandate": {
"command": "node",
"args": [
"/ABSOLUTE/PATH/TO/mandate/packages/mandate-mcp/dist/index.js"
],
"env": {
"MANDATE_AGENT_ID": "1",
"MANDATE_RPC_URL": "http://127.0.0.1:8545",
"MANDATE_SEQUENCER_URL": "http://127.0.0.1:8787",
"MANDATE_SESSION_KEY": "0x...",
"MANDATE_AUCTION_ADDRESS": "0x...",
"MANDATE_REGISTRY_ADDRESS": "0x...",
"MANDATE_CIRCUIT_PATH": "/ABSOLUTE/PATH/TO/mandate/circuits/policy_check",
"MANDATE_WHITELIST_ROOT": "0x...",
"MANDATE_MAX_ORDER_NOTIONAL": "1000000",
"MANDATE_MAX_POSITION": "1000",
"MANDATE_MAX_DAILY_LOSS": "500",
"MANDATE_POLICY_SALT": "42"
}
}
}
}Published npm package
{
"mcpServers": {
"mandate": {
"command": "npx",
"args": ["-y", "@0xgks/mandate-mcp"],
"env": {
"MANDATE_AGENT_ID": "1",
"MANDATE_RPC_URL": "http://127.0.0.1:8545",
"MANDATE_SEQUENCER_URL": "http://127.0.0.1:8787",
"MANDATE_SESSION_KEY": "0x...",
"MANDATE_AUCTION_ADDRESS": "0x...",
"MANDATE_REGISTRY_ADDRESS": "0x...",
"MANDATE_CIRCUIT_PATH": "/ABSOLUTE/PATH/TO/mandate/circuits/policy_check",
"MANDATE_WHITELIST_ROOT": "0x...",
"MANDATE_MAX_ORDER_NOTIONAL": "1000000",
"MANDATE_MAX_POSITION": "1000",
"MANDATE_MAX_DAILY_LOSS": "500",
"MANDATE_POLICY_SALT": "42"
}
}
}
}Give the agent only this MCP server (plus, if desired, read-only MoonPay tools) — see the security warning above.
Test prompts
Once registered, these four prompts exercise every tool this server exposes. Use them verbatim in the MoonPay Agent (or Claude/Codex) chat:
Epoch test
Use the mandate_get_epoch tool and tell me the current epoch, phase, and breaker status.Portfolio test
Use the mandate_get_portfolio tool and summarize the current portfolio state for the configured agent.Valid order test
Use only mandate_submit_order to submit a buy order for 500 units at a price of 3500. Do not use any unrestricted wallet, swap, transfer, or MoonPay execution tool.Invalid order test
Use only mandate_submit_order to attempt a buy order for 10000 units at a price of 3500. Return the exact policy or proof rejection reason. Do not bypass the MANDATE policy engine.Whether the "valid order" prompt's 500 units actually clears depends on the configured agent's registered
MANDATE_MAX_ORDER_NOTIONAL— see the note undermandate_submit_orderbelow. Against the demo's default mandate (cap1,000,000), 500 × 3500 = 1,750,000 would actually be rejected; either raise the amount's own agent's cap or use a smaller compliant amount (e.g. 250) when testing against the default demo policy.
Tools
mandate_get_portfolio
Read-only. No arguments. Returns this agent's current position, daily PnL, and anchored state root (cross-checked against the on-chain root).
mandate_get_epoch
Read-only. No arguments. Returns the current batch-auction epoch, phase
("commit" or "reveal"), and circuit-breaker bit.
mandate_submit_order
Generates a real zero-knowledge compliance proof and submits the proof-gated order through MANDATE. This is the only tool in this server that can put an order in the batch auction; there is no separate execution or signing tool, and there is no preview/dry-run mode. A preview is informational only does not apply here because there is no preview tool — only successful proof generation and protocol acceptance authorize an order.
Input:
{
"market": "ETH/USDC",
"side": "buy",
"amount": "250",
"limitPrice": "3500"
}amount and limitPrice are positive whole-number strings (the circuit's
size/limitPrice fields are u32; fractional amounts are rejected by
input validation, not silently truncated). market may be a symbol from
MANDATE_MARKET_MAP (default: ETH/USDC, ETH, WBTC/USDC, WBTC) or a
raw numeric circuit market id. Note that amount * limitPrice must stay
within the agent's registered MANDATE_MAX_ORDER_NOTIONAL — with the demo's
default mandate (1,000,000), 250 × 3500 = 875,000 is compliant while
10,000 × 3500 = 35,000,000 (see the rejected example below) is not.
Valid order → real proof + submission
{
"status": "submitted",
"submitted": true,
"orderCommitment": "0x022eb705af984a8e1682756a02238b58d065777e9588fa4fc477e58bb4be9912",
"epoch": "7",
"txHash": "0x39ad1cb0736b93eac40cd808861f878abd6032406fb85b88480119cd1ee7b26d"
}(Captured from a real local run against a freshly deployed Anvil stack —
independently confirmed on-chain via
cast call <auction> "committedIn(uint64,bytes32)(bool)" 7 0x022eb7... → true.)
Rejected order → mandate violation, nothing touches the chain
{
"status": "rejected",
"submitted": false,
"reason": "Assertion failed: order notional exceeds mandate maximum"
}isError: true is set on the MCP tool result whenever submitted is
false. The reason string is the real Noir circuit assertion message —
this server preserves it rather than replacing it with a generic error.
Error handling
- Full error detail (including stack traces) is only ever written to stderr, never returned to the MCP client.
- Tool results carry a short, sanitized
reasonstring. For the SDK's own error types (MandateViolationError,PolicyMismatchError,PortfolioMismatchError,EpochClosedError) this is the real, meaningful message (none of them embed key material). For anything else, it's the first line of the error message, length-capped. - The session private key is never included in any tool response or log line.
Known limitations
MandateClientdoes not expose a public portfolio- or epoch-reading method, somandate_get_portfolio/mandate_get_epochcall the same read-only contract views and sequencer endpoint thatMandateClient.proveAndSubmituses internally, directly from this package — not through the SDK. No proving, commitment, or Merkle logic is duplicated; these are plain reads.- There is no
mandate_preview_ordertool:@0xgks/mandate-sdkexposes no preview/dry-run method, and one is not invented here. MANDATE_MARKET_MAPis a convenience layer translating human-readable market symbols to the protocol's numeric market ids; it is not part of the MANDATE protocol itself.- This server assumes a single configured agent identity per process (one
MANDATE_AGENT_ID/session key pair). Running multiple agents means running multiple server processes with different environments.
