cordon-mcp 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Youvandra Febrial
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,7 +1,35 @@
1
1
  # cordon-mcp
2
2
 
3
- Cordon inside an MCP client. One config block, and the agent in front of you is
4
- bounded by a contract it cannot reach.
3
+ **An agent that can buy things and cannot spend past the mandate its owner
4
+ signed.** One config block, and
5
+ the assistant in front of you spends against a mandate enforced by a contract
6
+ it has no way to reach.
7
+
8
+ [![npm](https://img.shields.io/npm/v/cordon-mcp?color=6E56CF)](https://www.npmjs.com/package/cordon-mcp)
9
+ [![Node](https://img.shields.io/badge/Node-22%2B-5FA04E)](#requirements)
10
+ [![x402](https://img.shields.io/badge/x402-exact-6E56CF)](#paying-for-things)
11
+ [![ERC-8004](https://img.shields.io/badge/ERC--8004-identity%20%2B%20reputation-B5342A)](#the-record)
12
+ [![Licence](https://img.shields.io/badge/licence-MIT-blue)](LICENSE)
13
+
14
+ The limit is not a prompt, a policy or a wrapper around a key. It is a mandate
15
+ on chain, and every draw debits every ancestor up to the root — so a tree of
16
+ agents cannot spend past a total none of them can see. A refusal comes back as
17
+ an answer the agent can read, never as a crash, and it is written to the
18
+ agent's own record whether or not anyone is watching.
19
+
20
+ **Contents** ·
21
+ [Install](#install) ·
22
+ [Environment](#environment) ·
23
+ [Tools](#the-three-tools) ·
24
+ [Refusal reasons](#why-a-purchase-was-refused) ·
25
+ [A session](#a-session-end-to-end) ·
26
+ [What holds what](#what-holds-what) ·
27
+ [Chains](#chains) ·
28
+ [Troubleshooting](#troubleshooting)
29
+
30
+ ## Install
31
+
32
+ Nothing to install. Point your MCP client at it:
5
33
 
6
34
  ```json
7
35
  {
@@ -18,39 +46,222 @@ bounded by a contract it cannot reach.
18
46
  }
19
47
  ```
20
48
 
21
- `CORDON_ENV_FILE` is the key file `cordon-init` wrote, as an absolute path —
22
- an MCP client starts the server with no shell, so nothing expands `~` or
23
- `$HOME`. The key is read from that file and never appears in the client
24
- config. The contract addresses default to the Arc testnet deployment this
25
- version was built against; `CORDON_VAULT`, `CORDON_REGISTRY` and
26
- `CORDON_RECORD` override them. Several files can be listed, comma-separated.
49
+ Claude Desktop, Claude Code, Cursor and anything else speaking MCP over stdio
50
+ read a block of this shape. Restart the client after editing it.
51
+
52
+ ## Before that block works
53
+
54
+ This server speaks for a mandate that already exists. Two things have to be in
55
+ place, and both come from the main repository:
56
+
57
+ 1. **A mandate**, opened by an owner who funded it. The owner signs; nothing
58
+ here can open one for them.
59
+ 2. **A key file**, written by `cordon-init`. It holds the operator key for each
60
+ node, and this server reads it. The key never goes in the client config.
61
+
62
+ The [walkthrough](https://getcordon.xyz/docs/walkthrough) is the short path
63
+ through both.
64
+
65
+ ## Environment
66
+
67
+ | Variable | |
68
+ |---|---|
69
+ | `CORDON_ENV_FILE` | **Absolute path** to the key file `cordon-init` wrote. An MCP client starts the server with no shell, so nothing expands `~` or `$HOME`. Several files may be listed, comma-separated, in order. |
70
+ | `CORDON_MCP_NODE` | Which node this server speaks for, when the key file holds more than one. Without it the server takes whichever key parsed first, which is not a thing anybody can see. A node it holds no key for is refused by name. |
71
+ | `CORDON_VAULT` `CORDON_REGISTRY` `CORDON_RECORD` | The contracts. They default to the deployment this version was built against; an explicit value always wins. Set all three to point a published build at a different chain. |
72
+ | `CORDON_KEY_FILE` | Where a spawned child's key is written, before the spawn is sent. Defaults to the last file in `CORDON_ENV_FILE`. A child whose key was never written is a mandate nobody can sign for once this process restarts. |
73
+ | `CORDON_NODE_<label>` | One operator key per node, read from the key file rather than set by hand. `cordon-init` writes them; `CORDON_MCP_NODE` picks which one this server speaks for. |
74
+
75
+ A variable already set in the environment is never overwritten by the key file,
76
+ so the block above can override anything the file carries.
77
+
78
+ Nothing here takes a private key. `CORDON_ENV_FILE` is a path; the keys stay in
79
+ the file, which is `0600`, and the client config can be pasted into an issue
80
+ without leaking anything.
81
+
82
+ ## The three tools
83
+
84
+ ### `cordon_fetch`
85
+
86
+ Fetch a URL, and pay if the seller asks. The recipient and the price come from
87
+ the seller's own x402 challenge, never from the agent.
88
+
89
+ | Parameter | Type | |
90
+ |---|---|---|
91
+ | `url` | string, required | The URL to fetch. Must parse as a URL. |
92
+ | `method` | string, optional | HTTP method. Defaults to `GET`. |
93
+ | `body` | string, optional | Request body, for `POST` and friends. |
94
+
95
+ Three answers come back, and they read differently on purpose.
96
+
97
+ **Free** — the seller never asked for money:
98
+
99
+ ```
100
+ https://example.com/thing charged nothing.
101
+
102
+ <the body>
103
+ ```
104
+
105
+ **Paid**:
106
+
107
+ ```
108
+ Paid 1.00 USDC to 0x6302… on eip155:11155111.
109
+ Tranche credited to 0xe21a…
110
+ Draw 0x1525a8ce…
111
+
112
+ <the body>
113
+ ```
114
+
115
+ **Refused** — an answer, never an exception:
116
+
117
+ ```
118
+ REFUSED — the purchase is larger than one draw may be.
119
+
120
+ The purchase 2.00 USDC to 0x6302…
121
+ The bound that hit tranche-cap
122
+ Enforced at 0xe07a70dd…
123
+ On the record as refusal #1
124
+ Transaction 0x41962813…
125
+
126
+ Nothing was paid and no budget was consumed. This is a decision the
127
+ contract made; it cannot be retried past the bound that produced it.
128
+ ```
129
+
130
+ `Enforced at` is the node whose bound stopped it, which is often an ancestor
131
+ rather than the agent's own — the thing no per-agent limit can express.
132
+
133
+ ### `cordon_status`
134
+
135
+ No parameters.
136
+
137
+ ```
138
+ Can still draw 4.000000 USDC
139
+ Limited by 0x6052919a…
140
+ An ancestor is the binding constraint, not this node's own budget.
141
+ ```
142
+
143
+ The last line appears only when the limit is above this node.
144
+
145
+ ### `cordon_spawn`
146
+
147
+ A child mandate for a sub-agent, narrower than this one in every bound. The
148
+ contract refuses a wider child whoever asks.
27
149
 
28
- Node 22 or newer. The operator keys, the mandate and the daemon setup are in
29
- the [repository](https://github.com/youvandra/cordon) and at
30
- <https://getcordon.xyz/docs/walkthrough>.
150
+ | Parameter | Type | |
151
+ |---|---|---|
152
+ | `label` | string, required | What the sub-agent is for, in one line. Written beside its key and onto its ERC-8004 identity as a stated purpose. A description, not a bound. |
153
+ | `budgetUsdc` | string, required | Window budget in USDC, as a decimal string: `"25.00"`. A string so no float ever touches money. |
154
+ | `trancheUsdc` | string, optional | Per-draw cap. Defaults to the parent's. |
155
+ | `concentrationPct` | number, optional | Percent of the window one counterparty may take. Defaults to the parent's. |
31
156
 
32
- Where the keyring holds several nodes, `CORDON_MCP_NODE` names the one this
33
- server speaks for. Without it the server takes whichever key parsed first,
34
- which is not a thing anybody can see; a node it holds no key for is refused by
35
- name rather than silently falling back to another.
157
+ The child's key is generated here, written to the key file, and never returned
158
+ or logged. An agent that could read it would be an agent that holds a key.
36
159
 
37
- ## Three tools
160
+ ## Why a purchase was refused
161
+
162
+ Six reasons, and the contract returns exactly one. They are the same words the
163
+ console prints and the same words written to the record.
164
+
165
+ | Reason | |
166
+ |---|---|
167
+ | `revoked` | The mandate for this branch was cut. Nothing below a cut node spends again. |
168
+ | `tranche-cap` | The purchase is larger than one draw may be. |
169
+ | `window-budget` | The window is spent, on this node or an ancestor. |
170
+ | `concentration` | This recipient has taken its share of the window. |
171
+ | `vault-balance` | Every bound passed and the treasury is empty. |
172
+ | `lifetime-cap` | The total this mandate was signed for is spent, and it does not come back. |
173
+
174
+ Only `window-budget` and `concentration` pass with time, when the window rolls.
175
+ `lifetime-cap` and `revoked` never do. Splitting a purchase to get under a cap
176
+ is the behaviour the cap exists to catch, and the attempt is recorded.
177
+
178
+ ## A session, end to end
179
+
180
+ What an assistant holding a $5 mandate with a $1 per-draw cap actually sees.
181
+
182
+ ```
183
+ > cordon_status()
184
+ Can still draw 5.000000 USDC
185
+ Limited by this mandate
186
+
187
+ > cordon_fetch({ url: "https://demo-seller.example/snapshot" })
188
+ Paid 1.00 USDC to 0x6302… on eip155:11155111.
189
+ Draw 0x1525a8ce…
190
+
191
+ { "block": 11785656, "gasPrice": "1.04 gwei" }
192
+
193
+ > cordon_fetch({ url: "https://demo-seller.example/bulk" })
194
+ REFUSED — the purchase is larger than one draw may be.
195
+ The bound that hit tranche-cap
196
+ On the record as refusal #1
197
+
198
+ > cordon_status()
199
+ Can still draw 4.000000 USDC
200
+ ```
201
+
202
+ Two things in that transcript are the argument. The refusal consumed no
203
+ budget — headroom is `4.000000` after it, the same as before it — and it left
204
+ a numbered record the seller can read without asking anyone's permission.
205
+
206
+ <a id="paying-for-things"></a>
207
+
208
+ ## Paying for things
209
+
210
+ `cordon_fetch` answers an x402 `exact` challenge. The seller states its price
211
+ and its address; the agent states neither. What the agent can do is ask for a
212
+ URL, and what the contract decides is whether that purchase fits inside the
213
+ mandate. A purchase that does not fit comes back as a refusal with a reason and
214
+ the node that refused it.
215
+
216
+ <a id="the-record"></a>
217
+
218
+ ## The record
219
+
220
+ Every refusal is written to the node's ERC-8004 record, by the contract that
221
+ refused it. It is not a log this process keeps and could lose; it is on chain,
222
+ and it is readable by anyone deciding whether to serve this agent at all.
223
+
224
+ Publishing needs an ERC-8004 identity bound to the node, held by the node's
225
+ own operator. Without one, refusals are still enforced and simply go
226
+ unpublished — the bound never depends on the record.
227
+
228
+ ## What holds what
38
229
 
39
230
  | | |
40
231
  |---|---|
41
- | `cordon_fetch(url, method?, body?)` | fetch, and pay if the seller asks. The recipient and the price come from the seller's own challenge |
42
- | `cordon_status()` | what this mandate may still draw, and which node is the limit — often an ancestor |
43
- | `cordon_spawn(label, budgetUsdc, …)` | a child mandate, narrower than this one. The key stays here |
232
+ | **The agent** | Nothing. It can name a URL. It cannot name a recipient, an amount, or a key. |
233
+ | **This server** | The operator key for one node, read from a `0600` file. It signs draws and settlements and nothing else. |
234
+ | **The contracts** | The money, and every bound. A draw debits every ancestor up to the root before it releases anything. |
235
+ | **The owner** | The only key that can open a mandate, fund it, release a refusal or cut a branch. None of those are reachable from here. |
236
+
237
+ A key that reaches this process is still bounded: it can spend only what the
238
+ mandate already permits, and the tranche it draws is the size of the purchase
239
+ in front of it. Losing it is bounded by the same caps that bound normal use,
240
+ which is the difference between a bounded key and a hot wallet.
44
241
 
45
242
  ## The tools that are missing, permanently
46
243
 
47
- `cordon_transfer`, `cordon_pay`, `cordon_send`.
244
+ `cordon_transfer`, `cordon_pay`, `cordon_send`, `cordon_withdraw`,
245
+ `cordon_approve`.
48
246
 
49
247
  The absence is part of the fence rather than a gap in it. An agent that cannot
50
- express "send money to X" cannot be talked into it, and a test asserts each of
51
- those names stays unregistered. The tool list the website renders is
52
- introspected out of a live server over the real protocol, so a tool that failed
53
- to register disappears from the docs too.
248
+ express "send money to X" cannot be talked into it — by a prompt injection, a
249
+ confused user or its own reasoning. A test asserts each of those names stays
250
+ unregistered, and the tool list the website renders is introspected out of a
251
+ live server over the real protocol, so a tool that failed to register
252
+ disappears from the documentation too.
253
+
254
+ ## Chains
255
+
256
+ The rail follows the chain, and the server picks it rather than asking:
257
+
258
+ | | |
259
+ |---|---|
260
+ | **Arc testnet** (5042002) | Circle's Gateway. A draw lands in the operator's Gateway balance and settlement releases it, which costs Circle's fee on top of the price. Gas there is USDC, so an operator needs one asset. |
261
+ | **Ethereum Sepolia** (11155111) and anything else | The direct rail. A draw lands in the operator's own balance and settlement is the authorisation alone — no fee on top, and so no floor under the price. Gas is the chain's own token, so an operator needs two. |
262
+
263
+ Both are bounded identically. Caps, windows, concentration and the ancestor
264
+ debit are decided before a settler is reached.
54
265
 
55
266
  ## SKILL.md
56
267
 
@@ -67,6 +278,30 @@ node scripts/emit-skill.ts # rewrites SKILL.md
67
278
  node scripts/emit-tools.ts # rewrites the tool list the website renders
68
279
  ```
69
280
 
281
+ ## Troubleshooting
282
+
283
+ | What you see | What it is |
284
+ |---|---|
285
+ | `no node configured` | `CORDON_ENV_FILE` was not read. It must be an absolute path; a client starts this server with no shell. |
286
+ | `CORDON_MCP_NODE is 0x… and this process holds no key for it` | The key file holds other nodes. The message lists the ones it does hold. |
287
+ | The client shows no tools | stdout belongs to the protocol. If something in the chain prints to it, the transport is corrupt — this server sends every word of its own to stderr. |
288
+ | A purchase is refused | That is the product working. `cordon_status()` names the node that is the limit, and it is often an ancestor. |
289
+ | `no such mandate` on every call | The contracts this build defaults to are not the ones the node was opened in. A node id is derived from the registry address, so a redeployed registry means new node ids — set `CORDON_VAULT`, `CORDON_REGISTRY` and `CORDON_RECORD` together, or take a build matching the deployment. |
290
+ | Every draw fails with an insufficient-funds error | The operator has no gas. Off Arc that is the chain's own token and not the USDC it spends, so an operator with a tranche and no ETH cannot send the draw that would earn it. Fund the operator address, not the vault. |
291
+ | `cordon_spawn` succeeds and the child cannot sign later | The child's key is written to `CORDON_KEY_FILE`, which defaults to the last file in `CORDON_ENV_FILE`. If that path is not writable the spawn still lands on chain and the key is lost with the process. |
292
+
293
+ ## Requirements
294
+
295
+ Node 22 or newer.
296
+
297
+ ## Running it from a clone
298
+
299
+ ```bash
300
+ node /path/to/cordon/packages/mcp/src/main.ts
301
+ ```
302
+
303
+ Same environment, same server. The published build is this file bundled.
304
+
70
305
  ## Tests
71
306
 
72
307
  ```bash
@@ -79,3 +314,15 @@ seller answering a real 402 — everything a judge pasting the config block woul
79
314
  get, minus the GUI. `boot.test.ts` starts `main.ts` the way a client starts it,
80
315
  which is the one path the other two do not exercise, and which was broken for
81
316
  as long as nothing ran it.
317
+
318
+ ## Links
319
+
320
+ [Repository](https://github.com/youvandra/cordon) ·
321
+ [The argument](https://getcordon.xyz) ·
322
+ [Documentation](https://getcordon.xyz/docs) ·
323
+ [Owner's console](https://getcordon.xyz/console/) ·
324
+ [Changelog](https://github.com/youvandra/cordon/blob/main/CHANGELOG.md)
325
+
326
+ ## Licence
327
+
328
+ MIT. See [LICENSE](LICENSE).
package/SKILL.md CHANGED
@@ -10,13 +10,18 @@ description: >-
10
10
 
11
11
  # Cordon
12
12
 
13
- You can buy things. You cannot move money.
13
+ You can buy things. You cannot spend faster than the mandate your owner signed.
14
14
 
15
15
  The only spending tool you have takes a **URL**. It takes no recipient and no
16
16
  amount, and there is no tool here that sends money to an address. The price and
17
17
  the payee come from the seller's own payment challenge, and a contract on chain
18
18
  decides whether the purchase is allowed before any money exists.
19
19
 
20
+ Be clear about what that does and does not bound. Choosing a URL is choosing
21
+ who gets paid — no contract can read a seller's intent, so the payee is a
22
+ claim, recorded as one. What is enforced is how much and how fast, against
23
+ every ancestor up to the root, and every refusal is written on chain.
24
+
20
25
  ## What to call
21
26
 
22
27
  - `cordon_fetch(url, method?, body?)` — Fetch a URL. If it answers 402, pay for it through Cordon and return the body. The recipient and the price come from the seller's own challenge, not from you. May return a refusal, which is final.
@@ -78,14 +83,14 @@ buyer. That reading is itself a paid endpoint, priced at $0.01:
78
83
 
79
84
  | | |
80
85
  |---|---|
81
- | Chain | Arc testnet (5042002) |
82
- | Money | USDC, 6 decimals, at `0x3600000000000000000000000000000000000000` |
83
- | MandateRegistry | `0xf86de085e63b00c9fba300b19807c883deb961e9` |
84
- | TreeVault | `0x00ab57acd260c594a661b6101bdf7e92267af135` |
85
- | ConductRecord | `0x2a8361ac23f5ffcfde9f0d7bc7618178770332d0` |
86
- | Explorer | https://testnet.arcscan.app |
87
-
88
- Testnet. The gas and the money are both test USDC.
86
+ | Chain | Ethereum Sepolia (11155111) |
87
+ | Money | USDC, 6 decimals, at `0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238` |
88
+ | MandateRegistry | `0xe799edc4aa6bcaf6915c7a5eadbdc4e709aef4b2` |
89
+ | TreeVault | `0x22d539bdf23e08a856fc80bc991e34948921ad46` |
90
+ | ConductRecord | `0x5c026b1b129a9e2171e53c5024f76052006ef1e0` |
91
+ | Explorer | https://sepolia.etherscan.io |
92
+
93
+ Testnet. Gas is ETH and the money is USDC, so an operator needs both.
89
94
 
90
95
  ## The arrangement, stated plainly
91
96