csoai-gspc-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/README.md +60 -15
- package/gspc-tools.json +63 -9
- package/index.mjs +242 -6
- package/package.json +18 -7
- package/paid-tools.json +84 -0
- package/verify-card.mjs +5 -9
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# csoai-gspc-mcp
|
|
2
2
|
|
|
3
|
+
[](https://councilof.ai/gspc-scoreboard)
|
|
4
|
+
|
|
3
5
|
Stdio MCP server for the live GSPC board and the signed measurement cards at
|
|
4
6
|
[councilof.ai](https://councilof.ai). Zero dependencies. Node >= 20.
|
|
5
7
|
|
|
@@ -18,32 +20,67 @@ are reported as two labelled numbers and never reconciled.
|
|
|
18
20
|
| `get_axis` | One axis row from the live board: `n`, `accuracy`, `interval`, MEASURED/UNMEASURED status, dates. Args: `{ "axis": "jail" }`. |
|
|
19
21
|
| `verify_card` | Verify a signed `gspc.measurement-card` under the published rule — recompute the id from the canonical body, check the Ed25519 signature under the **pinned** key `did:web:csoai.org#card-attestation-1`. A card signed with its own freshly-made key is INVALID, not valid. Args: `{ "card": <object | JSON string | councilof.ai URL> }`. |
|
|
20
22
|
| `list_cards` | What the published index (`/signed/card_index.json`) declares next to what the card store endpoint (`/api/cards`) reports — two labelled numbers, never reconciled. Optional `axis`, `limit`. |
|
|
23
|
+
| `get_root` | GET `https://councilof.ai/root.json`. Three states: VALID / UNREACHABLE / UNCHECKABLE. Separate from GSPC. Never a certificate. |
|
|
24
|
+
| `get_card` | GET one card-v0 leaf by sha256. VALID / INVALID (not a leaf) / UNCHECKABLE (fetch failed). A 404 leaf is INVALID, not UNCHECKABLE. |
|
|
25
|
+
| `verify_inclusion` | GET `/api/proof?sha=`. VALID (included) / INVALID (not a leaf) / UNCHECKABLE (proof endpoint unreachable). |
|
|
26
|
+
|
|
27
|
+
Seven free tools above; five metered ones below. `tools/list` returns all twelve, and
|
|
28
|
+
`wired-tools.test.mjs` fails if a listed tool does not run or a running tool is not listed.
|
|
21
29
|
|
|
22
|
-
The same
|
|
30
|
+
The same seven free tools, from the same definitions file
|
|
23
31
|
(`functions/mcp/gspc-tools.json`), are served over HTTP at
|
|
24
32
|
`https://councilof.ai/mcp` (streamable HTTP, JSON-RPC 2.0 POST). Use whichever
|
|
25
33
|
transport your client speaks; the contracts are identical.
|
|
26
34
|
|
|
35
|
+
### The five x402-metered tools — carried here since 0.2.0
|
|
36
|
+
|
|
37
|
+
| tool | route | free path |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `commission_card` | `/api/request-attestation` | — (a payment never mints a MEASURED cell) |
|
|
40
|
+
| `art50_marking_evidence` | `/api/art50/marking-evidence` | `preview: true` |
|
|
41
|
+
| `rwa_evidence` | `/api/rwa/evidence` | `preview: true` (unsigned state) |
|
|
42
|
+
| `witness_hash` | `/api/witness` | — |
|
|
43
|
+
| `receipts_batch` | `/api/receipts/batch` | `preview: true` (count, span, roots, batch sha256) |
|
|
44
|
+
|
|
45
|
+
Payment travels as the **`x_payment` argument**, not as a transport header — so stdio carries these
|
|
46
|
+
exactly as the HTTP door does. Up to 0.1.1 this README said the opposite ("stdio has no payment header to
|
|
47
|
+
forward"); that was a statement about the transport, and it was wrong about the mechanism. The server
|
|
48
|
+
forwards your `x_payment` verbatim as the `X-PAYMENT` header on one request to `councilof.ai` and never
|
|
49
|
+
inspects, signs or invents a receipt. Settlement is the route's job, fail-closed.
|
|
50
|
+
|
|
51
|
+
Four honest statuses, and no fifth:
|
|
52
|
+
|
|
53
|
+
- **`PAYMENT_REQUIRED`** — the route answered 402. The full challenge (`accepts[]`, the `PAYMENT-REQUIRED`
|
|
54
|
+
header) comes back as `structuredContent`. Nothing was charged. A challenge is an answer, not a failure.
|
|
55
|
+
- **`DELIVERED`** — the route answered 2xx, with the settle echo when the route sent one.
|
|
56
|
+
- **`NOT_DEPLOYED`** — the route answered 404 on this origin. Said plainly, never a fabricated result.
|
|
57
|
+
- **`UNREACHABLE`** / **`BAD_ARGUMENTS`** — the call could not be made. Nothing was charged.
|
|
58
|
+
|
|
59
|
+
**Known limitation, stated rather than hidden (2026-09-04):** the free `preview` paths and the 402
|
|
60
|
+
challenge work today, but **settlement on the live rail is failing** — a genuine signed EIP-3009
|
|
61
|
+
authorization is rejected by the facilitator with HTTP 400 after the buyer signs, because the facilitator
|
|
62
|
+
now advertises two x402 dialects for Base and the wrong one is being selected. The fix is written and
|
|
63
|
+
tested but not merged. Until it is, treat the paid paths as: challenge yes, delivery no. This server
|
|
64
|
+
reports what the route actually said and never converts a failed settlement into a result.
|
|
65
|
+
|
|
66
|
+
Every paid deliverable is measurement, not certification; no tool on either transport carries a trust
|
|
67
|
+
label; amounts appear only inside a 402 challenge.
|
|
68
|
+
|
|
27
69
|
## Install
|
|
28
70
|
|
|
29
|
-
|
|
30
|
-
install from a checkout of the repo — the server is a single file with no
|
|
31
|
-
dependencies, so "install" is just pointing your client at it:
|
|
71
|
+
Published on npm as [`csoai-gspc-mcp`](https://www.npmjs.com/package/csoai-gspc-mcp). No checkout required:
|
|
32
72
|
|
|
33
73
|
```sh
|
|
34
|
-
|
|
35
|
-
# the server is at councilof-ai/mcp/gspc-server/index.mjs — no npm install needed
|
|
74
|
+
npx -y csoai-gspc-mcp
|
|
36
75
|
```
|
|
37
76
|
|
|
38
|
-
Below, replace `/ABS/PATH/TO/councilof-ai` with where you cloned it.
|
|
39
|
-
|
|
40
77
|
### Claude Code
|
|
41
78
|
|
|
42
79
|
```sh
|
|
43
|
-
claude mcp add gspc --
|
|
80
|
+
claude mcp add gspc -- npx -y csoai-gspc-mcp
|
|
44
81
|
```
|
|
45
82
|
|
|
46
|
-
|
|
83
|
+
From a checkout of the repo the server is `mcp/gspc-server/index.mjs` (no extra install).
|
|
47
84
|
|
|
48
85
|
### Claude Desktop
|
|
49
86
|
|
|
@@ -55,8 +92,8 @@ Add to `claude_desktop_config.json` (macOS:
|
|
|
55
92
|
{
|
|
56
93
|
"mcpServers": {
|
|
57
94
|
"gspc": {
|
|
58
|
-
"command": "
|
|
59
|
-
"args": ["
|
|
95
|
+
"command": "npx",
|
|
96
|
+
"args": ["-y", "csoai-gspc-mcp"]
|
|
60
97
|
}
|
|
61
98
|
}
|
|
62
99
|
}
|
|
@@ -70,16 +107,24 @@ Add to `.cursor/mcp.json` in your project (or `~/.cursor/mcp.json` globally):
|
|
|
70
107
|
{
|
|
71
108
|
"mcpServers": {
|
|
72
109
|
"gspc": {
|
|
73
|
-
"command": "
|
|
74
|
-
"args": ["
|
|
110
|
+
"command": "npx",
|
|
111
|
+
"args": ["-y", "csoai-gspc-mcp"]
|
|
75
112
|
}
|
|
76
113
|
}
|
|
77
114
|
}
|
|
78
115
|
```
|
|
79
116
|
|
|
117
|
+
### Grok Build
|
|
118
|
+
|
|
119
|
+
```toml
|
|
120
|
+
[mcp_servers.gspc-npm]
|
|
121
|
+
command = "npx"
|
|
122
|
+
args = ["-y", "csoai-gspc-mcp"]
|
|
123
|
+
```
|
|
124
|
+
|
|
80
125
|
### Any other stdio MCP client (Grok Bot, DSH harness, your own agent)
|
|
81
126
|
|
|
82
|
-
Spawn `
|
|
127
|
+
Spawn `npx -y csoai-gspc-mcp` and speak
|
|
83
128
|
newline-delimited JSON-RPC 2.0 on its stdin/stdout (stderr is logs only):
|
|
84
129
|
|
|
85
130
|
1. send `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"you","version":"0"}}}`
|
package/gspc-tools.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "csoai.gspc-mcp-tools/1",
|
|
3
|
-
"note": "ONE source of tool definitions for the GSPC MCP tools, shared by the stdio server (mcp/gspc-server/index.mjs) and the HTTP endpoint (functions/mcp/[[path]].ts) so the two surfaces cannot drift. Display text uses 'axis', never the plural
|
|
3
|
+
"note": "ONE source of tool definitions for the GSPC MCP tools, shared by the stdio server (mcp/gspc-server/index.mjs) and the HTTP endpoint (functions/mcp/[[path]].ts) so the two surfaces cannot drift. Display text uses 'axis', never the plural \u2014 owner language ruling; code identifiers are exempt.",
|
|
4
4
|
"tools": [
|
|
5
5
|
{
|
|
6
6
|
"name": "board_totals",
|
|
7
|
-
"description": "Live GSPC board totals from https://councilof.ai/api/gspc. Returns the slot count and the measured count as two labelled numbers WITH their kind
|
|
7
|
+
"description": "Live GSPC board totals from https://councilof.ai/api/gspc. Returns the slot count and the measured count as two labelled numbers WITH their kind \u2014 a slot is a declared position on the board, a measurement is a real run behind it; the two are never summed and never swapped \u2014 plus as_of dates for the board and for this fetch. We measure, never certify. If the board cannot be fetched the answer is a distinct UNREACHABLE state: no cached number is ever presented as live.",
|
|
8
8
|
"inputSchema": {
|
|
9
9
|
"type": "object",
|
|
10
10
|
"properties": {},
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
"name": "get_axis",
|
|
16
|
-
"description": "One axis row from the live GSPC board at https://councilof.ai/api/gspc: n, accuracy, interval, MEASURED or UNMEASURED status, family, and dates. An unmeasured axis is a first-class answer
|
|
16
|
+
"description": "One axis row from the live GSPC board at https://councilof.ai/api/gspc: n, accuracy, interval, MEASURED or UNMEASURED status, family, and dates. An unmeasured axis is a first-class answer \u2014 a declared slot with no run behind it, published so the gap is visible \u2014 never an error and never a zero. Never a certification.",
|
|
17
17
|
"inputSchema": {
|
|
18
18
|
"type": "object",
|
|
19
19
|
"properties": {
|
|
@@ -22,28 +22,39 @@
|
|
|
22
22
|
"description": "The axis name as it appears on the board, e.g. governance, safety, jail, provenance-controls. Case-insensitive. An unknown name returns the list of names the board actually carries."
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
|
-
"required": [
|
|
25
|
+
"required": [
|
|
26
|
+
"axis"
|
|
27
|
+
],
|
|
26
28
|
"additionalProperties": false
|
|
27
29
|
}
|
|
28
30
|
},
|
|
29
31
|
{
|
|
30
32
|
"name": "verify_card",
|
|
31
|
-
"description": "Verify a signed gspc.measurement-card under the published rule (https://councilof.ai/signed/HOW-TO-VERIFY.md): recompute the id from the canonical body bytes, then check the Ed25519 signature under the PINNED key published at did:web:csoai.org#card-attestation-1. A card that carries its own key proves only that the file is self-consistent
|
|
33
|
+
"description": "Verify a signed gspc.measurement-card under the published rule (https://councilof.ai/signed/HOW-TO-VERIFY.md): recompute the id from the canonical body bytes, then check the Ed25519 signature under the PINNED key published at did:web:csoai.org#card-attestation-1. A card that carries its own key proves only that the file is self-consistent \u2014 anyone can alter a body and sign it with a key they just generated \u2014 so a signer other than the published key is reported INVALID. Three verdicts, never two: VALID, INVALID (with the reason), or UNCHECKABLE when the check could not be completed \u2014 'could not check' is a different claim from 'forged'. Accepts the card as a JSON object, a JSON string, or a councilof.ai / csoai.org URL. Never a certification.",
|
|
32
34
|
"inputSchema": {
|
|
33
35
|
"type": "object",
|
|
34
36
|
"properties": {
|
|
35
37
|
"card": {
|
|
36
38
|
"description": "The signed card: an object, a JSON string, or a councilof.ai / csoai.org URL to one.",
|
|
37
|
-
"anyOf": [
|
|
39
|
+
"anyOf": [
|
|
40
|
+
{
|
|
41
|
+
"type": "object"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"type": "string"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
38
47
|
}
|
|
39
48
|
},
|
|
40
|
-
"required": [
|
|
49
|
+
"required": [
|
|
50
|
+
"card"
|
|
51
|
+
],
|
|
41
52
|
"additionalProperties": false
|
|
42
53
|
}
|
|
43
54
|
},
|
|
44
55
|
{
|
|
45
56
|
"name": "list_cards",
|
|
46
|
-
"description": "The published signed-card index (https://councilof.ai/signed/card_index.json): what the index declares (n_cards) and how many rows it actually carries, reported next to
|
|
57
|
+
"description": "The published signed-card index (https://councilof.ai/signed/card_index.json): what the index declares (n_cards) and how many rows it actually carries, reported next to \u2014 never reconciled with \u2014 the count the card store endpoint (https://councilof.ai/api/cards) reports for itself. Two labelled numbers from two surfaces; if they disagree, this tool shows the disagreement rather than picking one. Optional filters return recent rows: axis, limit.",
|
|
47
58
|
"inputSchema": {
|
|
48
59
|
"type": "object",
|
|
49
60
|
"properties": {
|
|
@@ -60,6 +71,49 @@
|
|
|
60
71
|
},
|
|
61
72
|
"additionalProperties": false
|
|
62
73
|
}
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"name": "get_root",
|
|
77
|
+
"description": "GET the permissionless public-root at https://councilof.ai/root.json. Returns merkle_root, card_count, as_of, and UNMEASURED notes. Separate from GSPC. Three states: VALID (fetched), UNREACHABLE (could not fetch), UNCHECKABLE (body unreadable). Never a certificate.",
|
|
78
|
+
"inputSchema": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"properties": {},
|
|
81
|
+
"additionalProperties": false
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"name": "get_card",
|
|
86
|
+
"description": "GET one public-root card-v0 leaf by sha256 (64 hex) from https://councilof.ai/cards/{sha16}.json. UNMEASURED cells stay visible. Three states: VALID (fetched), INVALID (not a leaf of the live root), UNCHECKABLE (fetch failed). Not a GSPC measurement-card.",
|
|
87
|
+
"inputSchema": {
|
|
88
|
+
"type": "object",
|
|
89
|
+
"properties": {
|
|
90
|
+
"sha256": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"description": "64-char hex payload SHA-256 of the card-v0 leaf."
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"required": [
|
|
96
|
+
"sha256"
|
|
97
|
+
],
|
|
98
|
+
"additionalProperties": false
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"name": "verify_inclusion",
|
|
103
|
+
"description": "Check a sha256 against the live public-root merkle via GET /api/proof?sha=. Three states only: VALID (included), INVALID (not a leaf), UNCHECKABLE (proof endpoint unreachable). Does not claim Ed25519 unless sig_ed25519 is present and checked separately. Never a grade.",
|
|
104
|
+
"inputSchema": {
|
|
105
|
+
"type": "object",
|
|
106
|
+
"properties": {
|
|
107
|
+
"sha256": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"description": "64-char hex digest to test for inclusion."
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"required": [
|
|
113
|
+
"sha256"
|
|
114
|
+
],
|
|
115
|
+
"additionalProperties": false
|
|
116
|
+
}
|
|
63
117
|
}
|
|
64
118
|
]
|
|
65
|
-
}
|
|
119
|
+
}
|
package/index.mjs
CHANGED
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
* numbers. This server never reconciles them.
|
|
19
19
|
*
|
|
20
20
|
* ONE SOURCE OF TRUTH:
|
|
21
|
-
* - Tool definitions come from gspc-tools.json
|
|
22
|
-
* endpoint at councilof.ai/mcp
|
|
21
|
+
* - Tool definitions come from gspc-tools.json (free) and paid-tools.json
|
|
22
|
+
* (x402-metered) — the same two files the HTTP endpoint at councilof.ai/mcp
|
|
23
|
+
* imports (functions/mcp/*.json).
|
|
23
24
|
* In a repo checkout that file is read directly; the npm package ships a
|
|
24
25
|
* byte-identical copy made at pack time (npm run prepack).
|
|
25
26
|
* - verify_card runs the code in public/signed/verify-card.mjs — the same
|
|
@@ -31,7 +32,7 @@ import { createInterface } from "node:readline";
|
|
|
31
32
|
import { readFileSync, existsSync } from "node:fs";
|
|
32
33
|
import { fileURLToPath } from "node:url";
|
|
33
34
|
|
|
34
|
-
const VERSION = "0.
|
|
35
|
+
const VERSION = "0.2.0";
|
|
35
36
|
const ORIGIN = process.env.GSPC_ORIGIN || "https://councilof.ai";
|
|
36
37
|
const FETCH_TIMEOUT_MS = 15000;
|
|
37
38
|
|
|
@@ -56,7 +57,24 @@ if (!TOOLS_PATH) {
|
|
|
56
57
|
process.stderr.write("csoai-gspc-mcp: gspc-tools.json not found — broken install\n");
|
|
57
58
|
process.exit(1);
|
|
58
59
|
}
|
|
59
|
-
const
|
|
60
|
+
const FREE_TOOLS = JSON.parse(readFileSync(TOOLS_PATH, "utf8")).tools;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The x402-metered tools, from the same definitions file the HTTP door uses.
|
|
64
|
+
*
|
|
65
|
+
* Payment travels as the `x_payment` TOOL ARGUMENT, not as a transport header, so stdio carries
|
|
66
|
+
* these exactly as the HTTP door does: the argument is forwarded verbatim as the X-PAYMENT header
|
|
67
|
+
* on one same-origin request, and this server never inspects, signs or invents a receipt.
|
|
68
|
+
* Settlement is the route's job, fail-closed. Without `x_payment` the tool returns the route's own
|
|
69
|
+
* 402 challenge, which is an answer and not a failure — and `preview` is free where a tool offers it.
|
|
70
|
+
*/
|
|
71
|
+
const PAID_TOOLS_PATH = firstExisting([
|
|
72
|
+
"../../functions/mcp/paid-tools.json", // repo checkout: the canonical file
|
|
73
|
+
"./paid-tools.json", // npm package: the byte-identical pack-time copy
|
|
74
|
+
]);
|
|
75
|
+
const PAID_TOOLS = PAID_TOOLS_PATH ? JSON.parse(readFileSync(PAID_TOOLS_PATH, "utf8")).tools : [];
|
|
76
|
+
const PAID_BY_NAME = new Map(PAID_TOOLS.map((t) => [t.name, t]));
|
|
77
|
+
const TOOLS = [...FREE_TOOLS, ...PAID_TOOLS];
|
|
60
78
|
|
|
61
79
|
const VERIFIER_PATH = firstExisting([
|
|
62
80
|
"../../public/signed/verify-card.mjs", // repo checkout: the canonical file
|
|
@@ -76,7 +94,11 @@ async function fetchJson(path) {
|
|
|
76
94
|
headers: { accept: "application/json" },
|
|
77
95
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
78
96
|
});
|
|
79
|
-
if (!r.ok)
|
|
97
|
+
if (!r.ok) {
|
|
98
|
+
const err = new Error(`GET ${url} returned HTTP ${r.status}`);
|
|
99
|
+
err.status = r.status;
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
80
102
|
return r.json();
|
|
81
103
|
}
|
|
82
104
|
|
|
@@ -271,11 +293,211 @@ async function listCards(args) {
|
|
|
271
293
|
return out;
|
|
272
294
|
}
|
|
273
295
|
|
|
296
|
+
async function getRoot() {
|
|
297
|
+
try {
|
|
298
|
+
const d = await fetchJson("/root.json");
|
|
299
|
+
return {
|
|
300
|
+
state: "VALID",
|
|
301
|
+
source: `${ORIGIN}/root.json`,
|
|
302
|
+
kind: d.kind ?? null,
|
|
303
|
+
as_of: d.as_of ?? null,
|
|
304
|
+
card_count: d.card_count ?? null,
|
|
305
|
+
merkle_root: d.merkle_root ?? null,
|
|
306
|
+
note: d.note ?? null,
|
|
307
|
+
not_a_certification: true,
|
|
308
|
+
not_gspc: true,
|
|
309
|
+
};
|
|
310
|
+
} catch (e) {
|
|
311
|
+
return { ...unreachable("/root.json", e), state: "UNREACHABLE", not_gspc: true };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function getCard(args) {
|
|
316
|
+
const sha = String(args.sha256 || "").trim().toLowerCase();
|
|
317
|
+
if (!/^[0-9a-f]{64}$/.test(sha)) {
|
|
318
|
+
return { state: "UNCHECKABLE", reason: "sha256 must be 64 hex", not_a_certification: true };
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
const d = await fetchJson(`/cards/${sha.slice(0, 16)}.json`);
|
|
322
|
+
const card = d.card || d;
|
|
323
|
+
const match = String(card.sha256 || "") === sha;
|
|
324
|
+
return {
|
|
325
|
+
state: match ? "VALID" : "INVALID",
|
|
326
|
+
sha256: sha,
|
|
327
|
+
source: `${ORIGIN}/cards/${sha.slice(0, 16)}.json`,
|
|
328
|
+
surface: card.surface ?? null,
|
|
329
|
+
unmeasured: card.unmeasured ?? [],
|
|
330
|
+
sig_ed25519: card.sig_ed25519 ?? null,
|
|
331
|
+
not_a_certification: true,
|
|
332
|
+
not_gspc: true,
|
|
333
|
+
};
|
|
334
|
+
} catch (e) {
|
|
335
|
+
if (e && e.status === 404) {
|
|
336
|
+
return {
|
|
337
|
+
state: "INVALID",
|
|
338
|
+
sha256: sha,
|
|
339
|
+
reason: "not a leaf of the live root",
|
|
340
|
+
source: `${ORIGIN}/cards/${sha.slice(0, 16)}.json`,
|
|
341
|
+
not_a_certification: true,
|
|
342
|
+
not_gspc: true,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
return { ...unreachable(`/cards/${sha.slice(0, 16)}.json`, e), state: "UNCHECKABLE", sha256: sha };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function verifyInclusion(args) {
|
|
350
|
+
const sha = String(args.sha256 || "").trim().toLowerCase();
|
|
351
|
+
if (!/^[0-9a-f]{64}$/.test(sha)) {
|
|
352
|
+
return { state: "UNCHECKABLE", reason: "sha256 must be 64 hex", not_a_certification: true };
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
const d = await fetchJson(`/api/proof?sha=${sha}`);
|
|
356
|
+
if (d.kind === "inclusion") {
|
|
357
|
+
return { state: "VALID", sha256: sha, merkle_root: d.merkle_root ?? null, not_a_certification: true };
|
|
358
|
+
}
|
|
359
|
+
if (d.error === "not_found") {
|
|
360
|
+
return { state: "INVALID", sha256: sha, reason: d.reason ?? "not a leaf", not_a_certification: true };
|
|
361
|
+
}
|
|
362
|
+
return { state: "UNCHECKABLE", sha256: sha, reason: d.reason ?? "unexpected proof body", not_a_certification: true };
|
|
363
|
+
} catch (e) {
|
|
364
|
+
if (e && e.status === 404) {
|
|
365
|
+
return { state: "INVALID", sha256: sha, reason: "not a leaf", not_a_certification: true };
|
|
366
|
+
}
|
|
367
|
+
return { ...unreachable(`/api/proof?sha=${sha}`, e), state: "UNCHECKABLE", sha256: sha };
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/* ---------------------------------------------------------------- paid tools */
|
|
372
|
+
|
|
373
|
+
const PAID_DOCTRINE =
|
|
374
|
+
"measurement, not certification — no tool here carries or awards a trust label of any kind; " +
|
|
375
|
+
"verification stays free";
|
|
376
|
+
|
|
377
|
+
/** Build the one request a paid tool makes. A caller cannot steer it to another URL. */
|
|
378
|
+
function buildPaidRequest(name, args) {
|
|
379
|
+
const tool = PAID_BY_NAME.get(name);
|
|
380
|
+
if (!tool) return { error: `unknown paid tool: ${name}` };
|
|
381
|
+
const route = tool.csoai.route;
|
|
382
|
+
const str = (k) => (typeof args[k] === "string" ? args[k].trim() : "");
|
|
383
|
+
const flag = (k) => args[k] === true || args[k] === "1" || args[k] === "true";
|
|
384
|
+
const headers = { accept: "application/json" };
|
|
385
|
+
const xp = str("x_payment");
|
|
386
|
+
if (xp) headers["x-payment"] = xp;
|
|
387
|
+
const u = new URL(route, ORIGIN);
|
|
388
|
+
let method = "GET";
|
|
389
|
+
let body;
|
|
390
|
+
|
|
391
|
+
switch (name) {
|
|
392
|
+
case "commission_card":
|
|
393
|
+
if (!str("subject")) return { error: "subject is required" };
|
|
394
|
+
u.searchParams.set("subject", str("subject"));
|
|
395
|
+
if (str("axis")) u.searchParams.set("axis", str("axis"));
|
|
396
|
+
break;
|
|
397
|
+
case "art50_marking_evidence":
|
|
398
|
+
if (flag("preview")) u.searchParams.set("preview", "1");
|
|
399
|
+
if (str("bytes_b64") || str("manifest_b64")) {
|
|
400
|
+
method = "POST";
|
|
401
|
+
headers["content-type"] = "application/json";
|
|
402
|
+
body = JSON.stringify(
|
|
403
|
+
str("bytes_b64") ? { bytes_b64: str("bytes_b64") } : { manifest_b64: str("manifest_b64") },
|
|
404
|
+
);
|
|
405
|
+
} else if (str("url")) {
|
|
406
|
+
u.searchParams.set("url", str("url"));
|
|
407
|
+
} else {
|
|
408
|
+
return { error: "one of url, bytes_b64 or manifest_b64 is required" };
|
|
409
|
+
}
|
|
410
|
+
break;
|
|
411
|
+
case "rwa_evidence":
|
|
412
|
+
if (!str("asset")) return { error: "asset is required" };
|
|
413
|
+
u.searchParams.set("asset", str("asset"));
|
|
414
|
+
if (flag("preview")) u.searchParams.set("preview", "1");
|
|
415
|
+
break;
|
|
416
|
+
case "witness_hash":
|
|
417
|
+
if (!str("sha256") && !str("url")) return { error: "sha256 or url is required" };
|
|
418
|
+
if (str("sha256")) u.searchParams.set("sha256", str("sha256").toLowerCase());
|
|
419
|
+
if (str("url")) u.searchParams.set("url", str("url"));
|
|
420
|
+
if (str("label")) u.searchParams.set("label", str("label"));
|
|
421
|
+
break;
|
|
422
|
+
case "receipts_batch":
|
|
423
|
+
if (!str("from")) return { error: "from is required (ISO-8601)" };
|
|
424
|
+
u.searchParams.set("from", str("from"));
|
|
425
|
+
if (str("to")) u.searchParams.set("to", str("to"));
|
|
426
|
+
if (flag("preview")) u.searchParams.set("preview", "1");
|
|
427
|
+
break;
|
|
428
|
+
default:
|
|
429
|
+
return { error: `no request builder for ${name}` };
|
|
430
|
+
}
|
|
431
|
+
return { url: u.toString(), init: { method, headers, ...(body ? { body } : {}) }, route };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Forward one paid tool to its route and report what came back, in the route's own words.
|
|
436
|
+
* 402 is PAYMENT_REQUIRED and not an error; 404 is NOT_DEPLOYED and never a fabricated result.
|
|
437
|
+
*/
|
|
438
|
+
async function callPaidTool(name, args) {
|
|
439
|
+
const tool = PAID_BY_NAME.get(name);
|
|
440
|
+
const base = {
|
|
441
|
+
tool: name,
|
|
442
|
+
route: tool.csoai.route,
|
|
443
|
+
sku: tool.csoai.sku,
|
|
444
|
+
rail: tool.csoai.rail,
|
|
445
|
+
doctrine: PAID_DOCTRINE,
|
|
446
|
+
not_a_certification: true,
|
|
447
|
+
};
|
|
448
|
+
const built = buildPaidRequest(name, args);
|
|
449
|
+
if (built.error) return { ...base, status: "BAD_ARGUMENTS", reason: built.error };
|
|
450
|
+
|
|
451
|
+
let res;
|
|
452
|
+
try {
|
|
453
|
+
res = await fetch(built.url, { ...built.init, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
454
|
+
} catch (e) {
|
|
455
|
+
return {
|
|
456
|
+
...base,
|
|
457
|
+
status: "UNREACHABLE",
|
|
458
|
+
reason: e instanceof Error ? e.message : String(e),
|
|
459
|
+
note: "The route could not be fetched. Nothing was charged, and no result is invented.",
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const text = await res.text();
|
|
464
|
+
let body = null;
|
|
465
|
+
try {
|
|
466
|
+
body = text ? JSON.parse(text) : null;
|
|
467
|
+
} catch {
|
|
468
|
+
body = { raw: text.slice(0, 2000) };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (res.status === 402) {
|
|
472
|
+
return {
|
|
473
|
+
...base,
|
|
474
|
+
status: "PAYMENT_REQUIRED",
|
|
475
|
+
http_status: 402,
|
|
476
|
+
payment_required: body,
|
|
477
|
+
payment_required_header: res.headers.get("payment-required"),
|
|
478
|
+
note:
|
|
479
|
+
"A challenge is an answer, not a failure. Pay from your own wallet against accepts[] and " +
|
|
480
|
+
"call again with x_payment. Nothing was charged by this call.",
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
if (res.status === 404) {
|
|
484
|
+
return { ...base, status: "NOT_DEPLOYED", http_status: 404, body, note: `${tool.csoai.route} is not on ${ORIGIN}.` };
|
|
485
|
+
}
|
|
486
|
+
if (res.ok) {
|
|
487
|
+
const settle = res.headers.get("x-payment-response");
|
|
488
|
+
return { ...base, status: "DELIVERED", http_status: res.status, body, ...(settle ? { x_payment_response: settle } : {}) };
|
|
489
|
+
}
|
|
490
|
+
return { ...base, status: String(res.status), http_status: res.status, body };
|
|
491
|
+
}
|
|
492
|
+
|
|
274
493
|
const HANDLERS = {
|
|
275
494
|
board_totals: boardTotals,
|
|
276
495
|
get_axis: getAxis,
|
|
277
496
|
verify_card: verifyCardTool,
|
|
278
497
|
list_cards: listCards,
|
|
498
|
+
get_root: getRoot,
|
|
499
|
+
get_card: getCard,
|
|
500
|
+
verify_inclusion: verifyInclusion,
|
|
279
501
|
};
|
|
280
502
|
|
|
281
503
|
/* ----------------------------------------------------------------- transport */
|
|
@@ -311,6 +533,20 @@ function summaryLine(name, payload) {
|
|
|
311
533
|
const b = payload.card_store_count_endpoint?.count ?? "?";
|
|
312
534
|
return `index declares ${a} card rows; the store's count endpoint reports ${b}. Two labelled numbers, not reconciled here.`;
|
|
313
535
|
}
|
|
536
|
+
case "get_root":
|
|
537
|
+
return `${payload.state ?? "?"} — public-root merkle ${(String(payload.merkle_root || "")).slice(0, 16) || "none"}. Not GSPC.`;
|
|
538
|
+
case "get_card":
|
|
539
|
+
return `${payload.state ?? "?"} — card-v0 leaf ${String(payload.sha256 || "").slice(0, 16) || "?"}.`;
|
|
540
|
+
case "verify_inclusion":
|
|
541
|
+
return `${payload.state ?? "?"} — inclusion against live merkle.`;
|
|
542
|
+
case "commission_card":
|
|
543
|
+
case "art50_marking_evidence":
|
|
544
|
+
case "rwa_evidence":
|
|
545
|
+
case "witness_hash":
|
|
546
|
+
case "receipts_batch":
|
|
547
|
+
return `${payload.status ?? "?"} — ${payload.route ?? name}${
|
|
548
|
+
payload.status === "PAYMENT_REQUIRED" ? "; nothing charged" : ""
|
|
549
|
+
}${payload.reason ? " — " + payload.reason : ""}. ${PAID_DOCTRINE}.`;
|
|
314
550
|
default:
|
|
315
551
|
return name;
|
|
316
552
|
}
|
|
@@ -334,7 +570,7 @@ async function handle(msg) {
|
|
|
334
570
|
|
|
335
571
|
if (method === "tools/call") {
|
|
336
572
|
const name = params?.name;
|
|
337
|
-
const fn = HANDLERS[name];
|
|
573
|
+
const fn = PAID_BY_NAME.has(name) ? (a) => callPaidTool(name, a) : HANDLERS[name];
|
|
338
574
|
if (!fn) return replyError(id, -32602, `unknown tool: ${name}`);
|
|
339
575
|
try {
|
|
340
576
|
const payload = await fn(params?.arguments ?? {});
|
package/package.json
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "csoai-gspc-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Stdio MCP server for the live GSPC board
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Stdio MCP server for the live GSPC board, signed measurement cards, and public-root merkle at councilof.ai. Twelve tools: seven free readers (board_totals, get_axis, verify_card with a pinned key and a three-state verdict, list_cards, get_root, get_card, verify_inclusion) and five x402-metered ones (commission_card, art50_marking_evidence, rwa_evidence, witness_hash, receipts_batch) that return the route's own 402 challenge until you pay. We measure, never certify; unmeasured is first-class; verdicts are VALID / INVALID / UNCHECKABLE, never two-state. A 404 leaf is INVALID. Zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"csoai-gspc-mcp": "index.mjs"
|
|
8
8
|
},
|
|
9
9
|
"main": "index.mjs",
|
|
10
10
|
"files": [
|
|
11
|
-
"
|
|
11
|
+
"README.md",
|
|
12
12
|
"gspc-tools.json",
|
|
13
|
-
"
|
|
14
|
-
"
|
|
13
|
+
"index.mjs",
|
|
14
|
+
"paid-tools.json",
|
|
15
|
+
"verify-card.mjs"
|
|
15
16
|
],
|
|
16
17
|
"scripts": {
|
|
17
18
|
"prepack": "node pack.mjs",
|
|
18
|
-
"smoke": "node smoke.mjs"
|
|
19
|
+
"smoke": "node smoke.mjs",
|
|
20
|
+
"test:wired": "node wired-tools.test.mjs"
|
|
19
21
|
},
|
|
20
22
|
"keywords": [
|
|
21
23
|
"mcp",
|
|
@@ -25,7 +27,16 @@
|
|
|
25
27
|
"measurement",
|
|
26
28
|
"ed25519",
|
|
27
29
|
"provenance",
|
|
28
|
-
"ai-governance"
|
|
30
|
+
"ai-governance",
|
|
31
|
+
"AI governance",
|
|
32
|
+
"model safety",
|
|
33
|
+
"EU AI Act",
|
|
34
|
+
"agent verification",
|
|
35
|
+
"model-safety",
|
|
36
|
+
"eu-ai-act",
|
|
37
|
+
"agent-verification",
|
|
38
|
+
"x402",
|
|
39
|
+
"attestation"
|
|
29
40
|
],
|
|
30
41
|
"license": "Apache-2.0",
|
|
31
42
|
"homepage": "https://councilof.ai",
|
package/paid-tools.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "csoai.gspc-mcp-paid-tools/1",
|
|
3
|
+
"note": "PAID tools on the MCP doors. Each wraps one x402-metered Pages route. Unpaid, the tool returns the route's 402 challenge as structuredContent (accepts[], PAYMENT-REQUIRED) so an MCP client pays from its own wallet and calls again with x_payment. Payment travels as the x_payment ARGUMENT, not as a transport header, so the stdio server (mcp/gspc-server, npm csoai-gspc-mcp >= 0.2.0) carries these too: it forwards the argument verbatim as the X-PAYMENT header on one request to councilof.ai and never inspects, signs or invents a receipt. Before 0.2.0 the stdio server carried only the seven free tools; that was a packaging choice, never a limit of the transport. Every deliverable is measurement, not certification; no tool here carries or awards a trust label of any kind, and the catalogue is free. Amounts appear only inside a 402 challenge, never here.",
|
|
4
|
+
"doctrine": "measurement, not certification",
|
|
5
|
+
"tools": [
|
|
6
|
+
{
|
|
7
|
+
"name": "commission_card",
|
|
8
|
+
"description": "PAID (x402). Commission one signed card-v0 receipt (surface ras.commission) for a named subject on the frozen bank via https://councilof.ai/api/request-attestation. Re-serves every signed measurement card already on file for the subject; a payment never mints a MEASURED cell (fresh_run stays UNMEASURED until a published run exists). Without x_payment the tool returns the 402 challenge (accepts[] with asset, amount, payTo) plus a free preview of the cards already on file. Measurement, not certification — never a rank, a grade or a certificate. Verification stays free.",
|
|
9
|
+
"inputSchema": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"properties": {
|
|
12
|
+
"subject": { "type": "string", "description": "Subject to commission: a model id, instrument id or card sha (1-120 chars of [A-Za-z0-9._:/@+-])." },
|
|
13
|
+
"axis": { "type": "string", "description": "Optional axis slug; omit for the subject-level commission." },
|
|
14
|
+
"x_payment": { "type": "string", "description": "The X-PAYMENT header value (base64 x402 payload) signed by your wallet against accepts[] from the previous 402. Omit to receive the challenge." }
|
|
15
|
+
},
|
|
16
|
+
"required": ["subject"],
|
|
17
|
+
"additionalProperties": false
|
|
18
|
+
},
|
|
19
|
+
"csoai": { "paid": true, "rail": "x402", "route": "/api/request-attestation", "sku": "request_attestation", "free_preview": "the 402 body (csoai.preview)", "deliverable": "one card-v0 leaf, surface ras.commission, Ed25519 when the Pages key is present" }
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"name": "art50_marking_evidence",
|
|
23
|
+
"description": "PAID (x402 or CSOAI LTD invoice). Article 50 marking-evidence pack via https://councilof.ai/api/art50/marking-evidence: is a machine-readable mark DETECTABLE in these bytes right now (C2PA manifest store, assertion hashes, hard binding, claim signature; IPTC digitalSourceType), beside the verbatim Art 50(2) excerpt hash and the Art 99(4) ceiling. Watermarks are UNCHECKABLE where no public detector exists and the pack says so. Point-in-time detection — never a conformity opinion, never a compliance word of any kind. preview=true is free and returns the same measurement unsigned. Without x_payment the tool returns the 402 challenge. If the route is not deployed on this origin the tool says NOT_DEPLOYED rather than inventing a result.",
|
|
24
|
+
"inputSchema": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"properties": {
|
|
27
|
+
"url": { "type": "string", "description": "Public http(s) URL of the generative output to measure (≤20 MiB)." },
|
|
28
|
+
"bytes_b64": { "type": "string", "description": "Alternatively the output bytes, base64 (≤20 MiB decoded)." },
|
|
29
|
+
"manifest_b64": { "type": "string", "description": "Alternatively a detached C2PA manifest store, base64 (manifest-only mode)." },
|
|
30
|
+
"preview": { "type": "boolean", "description": "true = free unsigned measurement (no card sha, no signature)." },
|
|
31
|
+
"x_payment": { "type": "string", "description": "The X-PAYMENT header value signed against accepts[] from the previous 402. Omit to receive the challenge." }
|
|
32
|
+
},
|
|
33
|
+
"additionalProperties": false
|
|
34
|
+
},
|
|
35
|
+
"csoai": { "paid": true, "rail": "x402-or-invoice", "route": "/api/art50/marking-evidence", "sku": "art50_marking_evidence", "free_preview": "preview=true", "deliverable": "one card-v0 leaf, surface art50.marking-evidence, kind csoai.art50.marking-evidence/0.1", "deployed_by": "live on councilof.ai (verified 2026-09-03)" }
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "rwa_evidence",
|
|
39
|
+
"description": "PAID (x402). Per-request signed evidence card of ONE XRPL issued asset's deterministic on-ledger state via https://councilof.ai/api/rwa/evidence: AccountRoot lsf* flags, Domain, the two-way xrp-ledger.toml check (PASS / FAIL / UNCHECKABLE — unreachable is never FAIL), gateway_balances obligation, holders as the free reader has them, every raw fetch sha256'd. preview=true is free (unsigned state). Historical state at fetched_at — not a rating, not a guarantee, not a conformity mark; /api/xrpl and /root.json stay free. Without x_payment the tool returns the 402 challenge. If the route is not deployed on this origin the tool says NOT_DEPLOYED.",
|
|
40
|
+
"inputSchema": {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"properties": {
|
|
43
|
+
"asset": { "type": "string", "description": "Symbol (as listed by https://councilof.ai/api/xrpl) or issuer r-address." },
|
|
44
|
+
"preview": { "type": "boolean", "description": "true = free unsigned state." },
|
|
45
|
+
"x_payment": { "type": "string", "description": "The X-PAYMENT header value signed against accepts[] from the previous 402. Omit to receive the challenge." }
|
|
46
|
+
},
|
|
47
|
+
"required": ["asset"],
|
|
48
|
+
"additionalProperties": false
|
|
49
|
+
},
|
|
50
|
+
"csoai": { "paid": true, "rail": "x402", "route": "/api/rwa/evidence", "sku": "rwa_evidence", "free_preview": "preview=true", "deliverable": "one card-v0 leaf, surface public.notice, kind csoai.eater.xrpl-issuer/0.1", "deployed_by": "live on councilof.ai (verified 2026-09-03)" }
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "witness_hash",
|
|
54
|
+
"description": "PAID (x402 or CSOAI LTD invoice). Witness a SHA-256 digest — 'attest what you're shown' — via https://councilof.ai/api/witness: one public.notice leaf (csoai.witness.hash/0.1) in the next hourly signed public root, an RFC-3161 timestamp over the digest, and the root's Rekor + OpenTimestamps anchors. Hash-only: name a digest, or a public URL fetched once (robots honoured, never past a login, paywall or bot check → UNCHECKABLE, nothing charged). Attests existence of the digest at the root's as_of — nothing about content, legality or provenance; a self-signed card carries no legal presumption. Measurement, not certification. Status is free at /api/witness/status. Without x_payment the tool returns the 402 challenge. If the route is not deployed on this origin the tool says NOT_DEPLOYED.",
|
|
55
|
+
"inputSchema": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"properties": {
|
|
58
|
+
"sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$", "description": "The digest to witness (64 lowercase hex). Or pass url." },
|
|
59
|
+
"url": { "type": "string", "description": "Optional public https URL, fetched once with our UA, hashed, never stored." },
|
|
60
|
+
"label": { "type": "string", "description": "Optional label (≤120 chars, no verdict words); appears on the leaf." },
|
|
61
|
+
"x_payment": { "type": "string", "description": "The X-PAYMENT header value signed against accepts[] from the previous 402. Omit to receive the challenge." }
|
|
62
|
+
},
|
|
63
|
+
"additionalProperties": false
|
|
64
|
+
},
|
|
65
|
+
"csoai": { "paid": true, "rail": "x402-or-invoice", "route": "/api/witness", "sku": "witness_hash", "free_status": "/api/witness/status?sha256=", "deliverable": "one public.notice leaf in the next hourly root + RFC-3161 reply + Rekor/OTS anchors", "deployed_by": "live on councilof.ai (verified 2026-09-03)" }
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"name": "receipts_batch",
|
|
69
|
+
"description": "PAID (x402 or CSOAI LTD invoice). A historical batch of the estate's measurement receipts via https://councilof.ai/api/receipts/batch: every signed card-v0 leaf whose as_of falls in [from,to] (≤200), each with its Merkle inclusion path and the public root(s) that carried it, plus the root index for the window and one signed manifest card citing the batch sha256. preview=true is free and returns count, span, root count and the sha256 of the exact bytes the paid call returns. Recent leaves are free at /root.json, /cards/ and /api/proof?sha= — the batch sells assembly across history, never a conclusion. No settlement-receipt stream exists (/api/receipts/latest is UNPUBLISHED) and this tool never claims one. Without x_payment the tool returns the 402 challenge.",
|
|
70
|
+
"inputSchema": {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"properties": {
|
|
73
|
+
"from": { "type": "string", "description": "Window start, ISO-8601. Required." },
|
|
74
|
+
"to": { "type": "string", "description": "Window end, ISO-8601. Defaults to now." },
|
|
75
|
+
"preview": { "type": "boolean", "description": "true = free: count, span, roots and batch sha256 without the leaves." },
|
|
76
|
+
"x_payment": { "type": "string", "description": "The X-PAYMENT header value signed against accepts[] from the previous 402. Omit to receive the challenge." }
|
|
77
|
+
},
|
|
78
|
+
"required": ["from"],
|
|
79
|
+
"additionalProperties": false
|
|
80
|
+
},
|
|
81
|
+
"csoai": { "paid": true, "rail": "x402-or-invoice", "route": "/api/receipts/batch", "sku": "receipts_batch", "free_preview": "preview=true", "deliverable": "csoai.receipts.batch/0.1 + one signed manifest card-v0 (surface receipts.batch)" }
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
package/verify-card.mjs
CHANGED
|
@@ -95,21 +95,17 @@ const unhex = (s) => Uint8Array.from(s.match(/../g).map((b) => parseInt(b, 16)))
|
|
|
95
95
|
* 2. ensure_ascii=False: non-ASCII stays literal (JS JSON.stringify does this), whereas
|
|
96
96
|
* v1 escapes to \\uXXXX (CPython ensure_ascii=True).
|
|
97
97
|
* 3. Same key-sort + compact separators + escape of quote/backslash/control chars.
|
|
98
|
-
* Matches harness/arena/jcs.py (Python
|
|
99
|
-
*
|
|
98
|
+
* Matches harness/arena/jcs.py (Python ES6 NumberToString, not repr).
|
|
99
|
+
* Conformance: jcs_conformance.py including 1e-6 / 1e-7 / 1e21 / -0.
|
|
100
100
|
*/
|
|
101
101
|
function canonicalJcs(value, key = null) {
|
|
102
102
|
if (value === null) return "null";
|
|
103
103
|
if (typeof value === "boolean") return value ? "true" : "false";
|
|
104
104
|
if (typeof value === "number") {
|
|
105
105
|
if (!Number.isFinite(value)) throw new Error(`non-finite number at ${key}`);
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (av >= 1e-6 && av < 1e21) return String(value);
|
|
110
|
-
}
|
|
111
|
-
if (value === 0) return "0"; // covers -0.0
|
|
112
|
-
return String(value);
|
|
106
|
+
// RFC 8785: ES6 Number.prototype.toString. ToString(−0) is "0". Not JSON.stringify
|
|
107
|
+
// (that maps NaN/∞ to null — already rejected above). Not CPython json.dumps (0.0).
|
|
108
|
+
return Number.prototype.toString.call(value);
|
|
113
109
|
}
|
|
114
110
|
if (typeof value === "string") return jcsString(value);
|
|
115
111
|
if (Array.isArray(value)) return "[" + value.map((v) => canonicalJcs(v, key)).join(",") + "]";
|