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 +21 -0
- package/README.md +271 -24
- package/SKILL.md +14 -9
- package/dist/cordon-mcp.js +677 -172
- package/package.json +7 -4
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
|
-
|
|
4
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/cordon-mcp)
|
|
9
|
+
[](#requirements)
|
|
10
|
+
[](#paying-for-things)
|
|
11
|
+
[](#the-record)
|
|
12
|
+
[](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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
|
42
|
-
|
|
|
43
|
-
|
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
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 |
|
|
82
|
-
| Money | USDC, 6 decimals, at `
|
|
83
|
-
| MandateRegistry | `
|
|
84
|
-
| TreeVault | `
|
|
85
|
-
| ConductRecord | `
|
|
86
|
-
| Explorer | https://
|
|
87
|
-
|
|
88
|
-
Testnet.
|
|
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
|
|