csoai-gspc-mcp 0.2.2 → 0.2.3-rc.2

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 CHANGED
@@ -1,6 +1,13 @@
1
1
  # csoai-gspc-mcp
2
2
 
3
- [![22 axes measured · 14 model fleets · 3 public leader scores · 8 fact runs · TIE is TIE · not a certificate. Three states only: VALID · INVALID · UNCHECKABLE.](https://councilof.ai/badge/gspc.svg)](https://councilof.ai/gspc-scoreboard)
3
+ [![npm version](https://img.shields.io/npm/v/csoai-gspc-mcp)](https://www.npmjs.com/package/csoai-gspc-mcp)
4
+ [![npm downloads](https://img.shields.io/npm/dm/csoai-gspc-mcp)](https://www.npmjs.com/package/csoai-gspc-mcp)
5
+ [![license](https://img.shields.io/npm/l/csoai-gspc-mcp)](https://github.com/CSOAI-ORG/councilof-ai/blob/master/LICENSE)
6
+
7
+
8
+ [![Live GSPC board badge — the counts are drawn live; read them from GET https://councilof.ai/api/gspc. Not a certificate. Three states only: VALID · INVALID · UNCHECKABLE.](https://councilof.ai/badge/gspc.svg)](https://councilof.ai/gspc-scoreboard)
9
+
10
+ <!-- No board totals or tool counts are typed in this README: they go stale between releases. The live board is GET https://councilof.ai/api/gspc (totals.public_count); the live tool list is tools/list. tools-match-door.test.ts fails if a count is typed here again. -->
4
11
 
5
12
  Stdio MCP server for the live GSPC board and the signed measurement cards at
6
13
  [councilof.ai](https://councilof.ai). Zero dependencies. Node >= 20.
@@ -24,16 +31,19 @@ are reported as two labelled numbers and never reconciled.
24
31
  | `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
32
  | `verify_inclusion` | GET `/api/proof?sha=`. VALID (included) / INVALID (not a leaf) / UNCHECKABLE (proof endpoint unreachable). |
26
33
  | `x402_trust` | Latest x402 catalog trust snapshot: counts of correct challenges and phantom resources. A 402 is a challenge, not delivery. |
34
+ | `mcp_trust` | Latest MCP handshake census snapshot: counts only. `partial: true` whenever the enumeration did not complete — a cap-limited read is a slice, never the population. |
27
35
 
28
- Eight free tools above; four metered ones below. `tools/list` returns all twelve, and
29
- `wired-tools.test.mjs` fails if a listed tool does not run or a running tool is not listed.
36
+ The free tools above and the metered ones below are exactly what `tools/list` returns — ask it for
37
+ the current set rather than trusting a number in a README. `wired-tools.test.mjs` fails if a listed
38
+ tool does not run or a running tool is not listed. Axis names are resolved case-insensitively through
39
+ one alias table (`axis-aliases.json`): `governance`, `gov` and `gspc-governance` are the same axis.
30
40
 
31
- The same eight free tools, from the same definitions file
41
+ The same free tools, from the same definitions file
32
42
  (`functions/mcp/gspc-tools.json`), are served over HTTP at
33
43
  `https://councilof.ai/mcp` (streamable HTTP, JSON-RPC 2.0 POST). Use whichever
34
44
  transport your client speaks; the contracts are identical.
35
45
 
36
- ### The four x402-metered tools
46
+ ### The x402-metered tools
37
47
 
38
48
  | tool | route | free path |
39
49
  |---|---|---|
@@ -51,10 +61,15 @@ Settlement is the route's job, fail-closed.
51
61
 
52
62
  Top-level statuses describe delivery, not settlement:
53
63
 
54
- - **`PAYMENT_REQUIRED`** — the route answered 402. The full challenge (`accepts[]`, the `PAYMENT-REQUIRED`
55
- header) comes back as `structuredContent`. With no `x_payment`, nothing was charged by that request. If
56
- an authorization was presented, settlement remains `UNCONFIRMED`; inspect before signing or retrying.
57
- A challenge is an answer, not a failure.
64
+ - **`PAYMENT_REQUIRED`** — the route answered 402. Following the x402 MCP transport
65
+ (`specs/transports-v2/mcp.md`), the tool result has `isError: true`, `structuredContent` carries the
66
+ route's `PaymentRequired` object at the top level (`x402Version`, `resource`, `accepts[]`, `extensions`)
67
+ alongside this wrapper's fields (`status`, `settlement_state`, …), and `content[0].text` is that same
68
+ object as JSON; the human summary is `content[1].text`. With no `x_payment`, nothing was charged by that
69
+ request. If an authorization was presented, settlement remains `UNCONFIRMED`; inspect before signing or
70
+ retrying. `isError` marks "not delivered yet" — it is a payment challenge, not a fault. Payment is still
71
+ read from the `x_payment` argument; `_meta["x402/payment"]` is not read yet, so a client that sends only
72
+ that gets the same challenge back and is charged nothing.
58
73
  - **`DELIVERED`** — the route answered 2xx and returned a deliverable. Inspect `delivery_kind`:
59
74
  `PREVIEW_OR_FREE`, `DELIVERED_SETTLEMENT_UNCONFIRMED`, `DELIVERED_RECEIPT_GAP`, or
60
75
  `DELIVERED_WITH_ROUTE_RECEIPT`. `receipt_state: PRESENT_UNVERIFIED` means a JWS-shaped receipt was
@@ -90,6 +105,9 @@ claude mcp add gspc -- npx -y csoai-gspc-mcp
90
105
 
91
106
  From a checkout of the repo the server is `mcp/gspc-server/index.mjs` (no extra install).
92
107
 
108
+ MCP Registry name: `ai.councilof/gspc` (canonical, domain-verified). `io.github.CSOAI-ORG/gspc` is its
109
+ deprecated alias for the same door.
110
+
93
111
  ### Claude Desktop
94
112
 
95
113
  Add to `claude_desktop_config.json` (macOS:
@@ -140,9 +158,10 @@ newline-delimited JSON-RPC 2.0 on its stdin/stdout (stderr is logs only):
140
158
  3. send `{"jsonrpc":"2.0","id":2,"method":"tools/list"}`
141
159
  4. call tools: `{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"board_totals","arguments":{}}}`
142
160
 
143
- Every `tools/call` result carries both a human `content[0].text` summary and a
144
- machine `structuredContent` object. Protocol versions accepted: 2024-11-05,
145
- 2025-03-26, 2025-06-18.
161
+ Every `tools/call` result carries a human text summary and a machine
162
+ `structuredContent` object (a payment challenge puts the challenge JSON first, see
163
+ above). Protocol versions accepted: 2024-11-05, 2025-03-26, 2025-06-18,
164
+ 2025-11-25; an unknown requested version is answered with 2025-11-25.
146
165
 
147
166
  If you cannot spawn processes, POST the same JSON-RPC bodies to
148
167
  `https://councilof.ai/mcp` instead.
@@ -167,6 +186,8 @@ card-attestation key) even though it is perfectly self-consistent.
167
186
 
168
187
  ## One source of truth
169
188
 
189
+ - Axis aliases: `functions/mcp/axis-aliases.json` — one table for this server, the
190
+ HTTP endpoint and the `csoai-gspc` Python client.
170
191
  - Tool definitions: `functions/mcp/gspc-tools.json` — shared byte-for-byte with
171
192
  the HTTP endpoint (`functions/mcp/[[path]].ts`). Neither surface defines
172
193
  these tools anywhere else.
@@ -0,0 +1,101 @@
1
+ {
2
+ "schema": "csoai.axis-aliases/1",
3
+ "note": "THE ONE axis alias table. Keys are the canonical axis ids exactly as GET https://councilof.ai/api/gspc spells them (axes[].axis). Values are the other spellings a caller meets: the board row's own dataset code (csoai/gspc-<code> -> <code> and gspc-<code>) and the gspc-<axis> spelling the signed card index uses. Nothing else is an alias: an index name with no grounded link to a board row (arc-30, care-refusal-help, jail-escape-detection, swarm-candidates, ...) stays its own name. Resolution is case-insensitive: trim, lowercase, canonical match first, then alias. Byte-identical copies ship in mcp/gspc-server/axis-aliases.json (npm) and scripts/spray/pypi/csoai-gspc/csoai_gspc/axis_aliases.json (PyPI); functions/mcp/axis-aliases.test.ts fails if a copy drifts or a key stops being a board row.",
4
+ "axes": {
5
+ "governance": [
6
+ "gov",
7
+ "gspc-gov",
8
+ "gspc-governance"
9
+ ],
10
+ "safety": [
11
+ "agi",
12
+ "gspc-agi",
13
+ "gspc-safety"
14
+ ],
15
+ "provenance": [
16
+ "prv",
17
+ "gspc-prv",
18
+ "gspc-provenance"
19
+ ],
20
+ "continuity": [
21
+ "asi",
22
+ "gspc-asi",
23
+ "gspc-continuity"
24
+ ],
25
+ "conformance": [
26
+ "mcp",
27
+ "gspc-mcp",
28
+ "gspc-conformance"
29
+ ],
30
+ "openness": [
31
+ "oss",
32
+ "gspc-oss",
33
+ "gspc-openness"
34
+ ],
35
+ "machinery-conformity": [
36
+ "mach",
37
+ "gspc-mach",
38
+ "gspc-machinery-conformity"
39
+ ],
40
+ "care": [
41
+ "gspc-care"
42
+ ],
43
+ "cross-reality": [
44
+ "xr",
45
+ "gspc-xr",
46
+ "gspc-cross-reality"
47
+ ],
48
+ "detector-interop": [
49
+ "det",
50
+ "gspc-det",
51
+ "gspc-detector-interop"
52
+ ],
53
+ "art5-safeguard": [
54
+ "art5",
55
+ "gspc-art5",
56
+ "gspc-art5-safeguard"
57
+ ],
58
+ "swarm": [
59
+ "gspc-swarm"
60
+ ],
61
+ "affect": [
62
+ "gspc-affect"
63
+ ],
64
+ "jail": [
65
+ "jail-goldbank",
66
+ "gspc-jail-goldbank",
67
+ "gspc-jail"
68
+ ],
69
+ "effect-binding": [
70
+ "gspc-effect-binding"
71
+ ],
72
+ "provenance-controls": [
73
+ "gspc-provenance-controls"
74
+ ],
75
+ "reserve-attestation": [
76
+ "gspc-reserve-attestation"
77
+ ],
78
+ "regulatory-framework": [
79
+ "gspc-regulatory-framework"
80
+ ],
81
+ "distribution-integrity": [
82
+ "gspc-distribution-integrity"
83
+ ],
84
+ "custody-disclosure": [
85
+ "gspc-custody-disclosure"
86
+ ],
87
+ "ai-adoption-components": [
88
+ "ai-economy-index",
89
+ "gspc-ai-economy-index",
90
+ "gspc-ai-adoption-components"
91
+ ],
92
+ "labour-components": [
93
+ "human-labour-index",
94
+ "gspc-human-labour-index",
95
+ "gspc-labour-components"
96
+ ],
97
+ "humanoid-labour-index": [
98
+ "gspc-humanoid-labour-index"
99
+ ]
100
+ }
101
+ }
package/gspc-tools.json CHANGED
@@ -4,16 +4,66 @@
4
4
  "tools": [
5
5
  {
6
6
  "name": "board_totals",
7
+ "title": "Live GSPC board totals",
7
8
  "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
9
  "inputSchema": {
9
10
  "type": "object",
10
11
  "properties": {},
11
12
  "additionalProperties": false
13
+ },
14
+ "outputSchema": {
15
+ "type": "object",
16
+ "properties": {
17
+ "state": {
18
+ "type": "string",
19
+ "description": "LIVE or UNREACHABLE"
20
+ },
21
+ "counts": {
22
+ "type": "array",
23
+ "items": {
24
+ "type": "object",
25
+ "properties": {
26
+ "name": {
27
+ "type": "string"
28
+ },
29
+ "kind": {
30
+ "type": "string"
31
+ }
32
+ }
33
+ }
34
+ },
35
+ "public_count": {
36
+ "type": [
37
+ "string",
38
+ "null"
39
+ ]
40
+ },
41
+ "count_grammar": {},
42
+ "by_family": {},
43
+ "as_of": {},
44
+ "source": {
45
+ "type": "string"
46
+ },
47
+ "not_a_certification": {
48
+ "type": "boolean"
49
+ }
50
+ },
51
+ "required": [
52
+ "state"
53
+ ]
54
+ },
55
+ "annotations": {
56
+ "title": "Live GSPC board totals",
57
+ "readOnlyHint": true,
58
+ "destructiveHint": false,
59
+ "idempotentHint": true,
60
+ "openWorldHint": true
12
61
  }
13
62
  },
14
63
  {
15
64
  "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 \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.",
65
+ "title": "One GSPC board axis",
66
+ "description": "One axis row from the live GSPC board at https://councilof.ai/api/gspc \u2014 every axis the board carries, behavioural and financial families alike, addressed by the axis id exactly as the board spells it: n, accuracy, interval, MEASURED or UNMEASURED status, family, kind, the bank or run-artifact URL behind the row, 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
67
  "inputSchema": {
18
68
  "type": "object",
19
69
  "properties": {
@@ -26,10 +76,64 @@
26
76
  "axis"
27
77
  ],
28
78
  "additionalProperties": false
79
+ },
80
+ "outputSchema": {
81
+ "type": "object",
82
+ "properties": {
83
+ "state": {
84
+ "type": "string",
85
+ "description": "LIVE, NOT_ON_BOARD, BAD_INPUT or UNREACHABLE"
86
+ },
87
+ "axis": {
88
+ "type": [
89
+ "string",
90
+ "null"
91
+ ]
92
+ },
93
+ "resolved_from": {
94
+ "type": "string",
95
+ "description": "present when the caller named an alias (e.g. gov) that resolved to this board axis"
96
+ },
97
+ "status": {
98
+ "type": [
99
+ "string",
100
+ "null"
101
+ ]
102
+ },
103
+ "measured": {
104
+ "type": "boolean"
105
+ },
106
+ "family": {},
107
+ "n": {},
108
+ "accuracy": {},
109
+ "interval": {},
110
+ "leader": {},
111
+ "dataset": {},
112
+ "board_carries": {
113
+ "type": "array"
114
+ },
115
+ "source": {
116
+ "type": "string"
117
+ },
118
+ "not_a_certification": {
119
+ "type": "boolean"
120
+ }
121
+ },
122
+ "required": [
123
+ "state"
124
+ ]
125
+ },
126
+ "annotations": {
127
+ "title": "One GSPC board axis",
128
+ "readOnlyHint": true,
129
+ "destructiveHint": false,
130
+ "idempotentHint": true,
131
+ "openWorldHint": true
29
132
  }
30
133
  },
31
134
  {
32
135
  "name": "verify_card",
136
+ "title": "Verify a signed measurement card (free)",
33
137
  "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.",
34
138
  "inputSchema": {
35
139
  "type": "object",
@@ -50,10 +154,97 @@
50
154
  "card"
51
155
  ],
52
156
  "additionalProperties": false
157
+ },
158
+ "outputSchema": {
159
+ "type": "object",
160
+ "properties": {
161
+ "state": {
162
+ "type": "string",
163
+ "description": "VALID, INVALID or UNCHECKABLE"
164
+ },
165
+ "id": {
166
+ "type": [
167
+ "string",
168
+ "null"
169
+ ]
170
+ },
171
+ "family": {
172
+ "type": [
173
+ "string",
174
+ "null"
175
+ ]
176
+ },
177
+ "reason": {
178
+ "type": [
179
+ "string",
180
+ "null"
181
+ ]
182
+ },
183
+ "reasons": {
184
+ "type": "array",
185
+ "items": {
186
+ "type": "string"
187
+ }
188
+ },
189
+ "checks": {
190
+ "type": "array",
191
+ "items": {
192
+ "type": "object",
193
+ "properties": {
194
+ "check": {
195
+ "type": "string"
196
+ },
197
+ "ok": {
198
+ "type": [
199
+ "boolean",
200
+ "null"
201
+ ],
202
+ "description": "true = passed, false = failed, null = not applicable or not run (detail says UNCHECKED)"
203
+ },
204
+ "code": {
205
+ "type": "string"
206
+ },
207
+ "detail": {
208
+ "type": "string"
209
+ },
210
+ "advisory": {
211
+ "type": "boolean",
212
+ "description": "true = reported but never decides the verdict (the live did.json cross-check)"
213
+ }
214
+ }
215
+ }
216
+ },
217
+ "pinned_key": {
218
+ "type": [
219
+ "string",
220
+ "null"
221
+ ]
222
+ },
223
+ "rule": {
224
+ "type": "string"
225
+ },
226
+ "note": {
227
+ "type": "string"
228
+ },
229
+ "not_a_certification": {
230
+ "type": "boolean"
231
+ }
232
+ },
233
+ "required": [
234
+ "state"
235
+ ]
236
+ },
237
+ "annotations": {
238
+ "title": "Verify a signed measurement card (free)",
239
+ "readOnlyHint": true,
240
+ "destructiveHint": false,
241
+ "idempotentHint": true,
242
+ "openWorldHint": true
53
243
  }
54
244
  },
55
245
  {
56
246
  "name": "list_cards",
247
+ "title": "List signed card index rows",
57
248
  "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.",
58
249
  "inputSchema": {
59
250
  "type": "object",
@@ -70,19 +261,127 @@
70
261
  }
71
262
  },
72
263
  "additionalProperties": false
264
+ },
265
+ "outputSchema": {
266
+ "type": "object",
267
+ "properties": {
268
+ "index": {
269
+ "type": [
270
+ "object",
271
+ "null"
272
+ ]
273
+ },
274
+ "card_store_count_endpoint": {
275
+ "type": [
276
+ "object",
277
+ "null"
278
+ ]
279
+ },
280
+ "rows": {
281
+ "type": [
282
+ "array",
283
+ "null"
284
+ ],
285
+ "items": {
286
+ "type": "object",
287
+ "properties": {
288
+ "card": {
289
+ "type": "string"
290
+ },
291
+ "axis": {
292
+ "type": "string"
293
+ },
294
+ "ts": {},
295
+ "signed": {}
296
+ }
297
+ }
298
+ },
299
+ "axis_query": {
300
+ "type": "object",
301
+ "properties": {
302
+ "asked": {
303
+ "type": "string"
304
+ },
305
+ "canonical": {
306
+ "type": "string"
307
+ },
308
+ "spellings": {
309
+ "type": "array",
310
+ "items": {
311
+ "type": "string"
312
+ }
313
+ },
314
+ "index_names_matched": {
315
+ "type": "array",
316
+ "items": {
317
+ "type": "string"
318
+ }
319
+ }
320
+ }
321
+ },
322
+ "doctrine": {
323
+ "type": "string"
324
+ },
325
+ "not_a_certification": {
326
+ "type": "boolean"
327
+ }
328
+ }
329
+ },
330
+ "annotations": {
331
+ "title": "List signed card index rows",
332
+ "readOnlyHint": true,
333
+ "destructiveHint": false,
334
+ "idempotentHint": true,
335
+ "openWorldHint": true
73
336
  }
74
337
  },
75
338
  {
76
339
  "name": "get_root",
340
+ "title": "Public Merkle root",
77
341
  "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
342
  "inputSchema": {
79
343
  "type": "object",
80
344
  "properties": {},
81
345
  "additionalProperties": false
346
+ },
347
+ "outputSchema": {
348
+ "type": "object",
349
+ "properties": {
350
+ "state": {
351
+ "type": "string",
352
+ "description": "VALID, UNREACHABLE or UNCHECKABLE"
353
+ },
354
+ "merkle_root": {
355
+ "type": [
356
+ "string",
357
+ "null"
358
+ ]
359
+ },
360
+ "card_count": {},
361
+ "as_of": {},
362
+ "kind": {},
363
+ "source": {
364
+ "type": "string"
365
+ },
366
+ "not_gspc": {
367
+ "type": "boolean"
368
+ }
369
+ },
370
+ "required": [
371
+ "state"
372
+ ]
373
+ },
374
+ "annotations": {
375
+ "title": "Public Merkle root",
376
+ "readOnlyHint": true,
377
+ "destructiveHint": false,
378
+ "idempotentHint": true,
379
+ "openWorldHint": true
82
380
  }
83
381
  },
84
382
  {
85
383
  "name": "get_card",
384
+ "title": "One public-root card-v0 leaf",
86
385
  "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
386
  "inputSchema": {
88
387
  "type": "object",
@@ -96,10 +395,45 @@
96
395
  "sha256"
97
396
  ],
98
397
  "additionalProperties": false
398
+ },
399
+ "outputSchema": {
400
+ "type": "object",
401
+ "properties": {
402
+ "state": {
403
+ "type": "string",
404
+ "description": "VALID, INVALID or UNCHECKABLE"
405
+ },
406
+ "sha256": {
407
+ "type": "string"
408
+ },
409
+ "reason": {
410
+ "type": "string"
411
+ },
412
+ "source": {
413
+ "type": "string"
414
+ },
415
+ "surface": {},
416
+ "unmeasured": {},
417
+ "sig_ed25519": {},
418
+ "not_gspc": {
419
+ "type": "boolean"
420
+ }
421
+ },
422
+ "required": [
423
+ "state"
424
+ ]
425
+ },
426
+ "annotations": {
427
+ "title": "One public-root card-v0 leaf",
428
+ "readOnlyHint": true,
429
+ "destructiveHint": false,
430
+ "idempotentHint": true,
431
+ "openWorldHint": true
99
432
  }
100
433
  },
101
434
  {
102
435
  "name": "verify_inclusion",
436
+ "title": "Merkle inclusion check",
103
437
  "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
438
  "inputSchema": {
105
439
  "type": "object",
@@ -113,11 +447,41 @@
113
447
  "sha256"
114
448
  ],
115
449
  "additionalProperties": false
450
+ },
451
+ "outputSchema": {
452
+ "type": "object",
453
+ "properties": {
454
+ "state": {
455
+ "type": "string",
456
+ "description": "VALID, INVALID or UNCHECKABLE"
457
+ },
458
+ "sha256": {
459
+ "type": "string"
460
+ },
461
+ "merkle_root": {
462
+ "type": [
463
+ "string",
464
+ "null"
465
+ ]
466
+ },
467
+ "reason": {}
468
+ },
469
+ "required": [
470
+ "state"
471
+ ]
472
+ },
473
+ "annotations": {
474
+ "title": "Merkle inclusion check",
475
+ "readOnlyHint": true,
476
+ "destructiveHint": false,
477
+ "idempotentHint": true,
478
+ "openWorldHint": true
116
479
  }
117
480
  },
118
481
  {
119
482
  "name": "x402_trust",
120
- "description": "GET the latest x402 catalog trust snapshot: counts of how many catalogued x402 resources open a correct 402 challenge vs how many are phantom on the wire. Counts only by doctrine — host details withheld; a 402 is NOT delivery; measurement, never certification.",
483
+ "title": "x402 catalog trust snapshot",
484
+ "description": "GET the latest x402 catalog trust snapshot: counts of how many catalogued x402 resources open a correct 402 challenge vs how many are phantom on the wire. Counts only by doctrine \u2014 host details withheld; a 402 is NOT delivery; measurement, never certification.",
121
485
  "inputSchema": {
122
486
  "type": "object",
123
487
  "properties": {},
@@ -126,16 +490,313 @@
126
490
  "outputSchema": {
127
491
  "type": "object",
128
492
  "properties": {
493
+ "state": {
494
+ "type": "string",
495
+ "description": "VALID or UNREACHABLE"
496
+ },
129
497
  "counts": {
130
- "type": "object"
498
+ "type": [
499
+ "object",
500
+ "null"
501
+ ]
131
502
  },
132
503
  "headline": {
504
+ "type": [
505
+ "string",
506
+ "null"
507
+ ]
508
+ },
509
+ "as_of": {
510
+ "type": [
511
+ "string",
512
+ "null"
513
+ ]
514
+ },
515
+ "kind": {},
516
+ "source": {
133
517
  "type": "string"
518
+ }
519
+ },
520
+ "required": [
521
+ "state"
522
+ ]
523
+ },
524
+ "annotations": {
525
+ "title": "x402 catalog trust snapshot",
526
+ "readOnlyHint": true,
527
+ "destructiveHint": false,
528
+ "idempotentHint": true,
529
+ "openWorldHint": true
530
+ }
531
+ },
532
+ {
533
+ "name": "mcp_trust",
534
+ "title": "MCP handshake census snapshot",
535
+ "description": "GET the latest MCP handshake trust snapshot (https://councilof.ai/interop/mcp-trust/latest.json): counts of how many internet-facing MCP servers answer a correct initialize handshake, how many respond with an auth challenge, and how many are unreachable. Counts only by doctrine \u2014 host details withheld by design. A partial round or a cap change is disclosed in the snapshot. Measurement, never certification. Three states: VALID (fetched), UNREACHABLE (could not fetch), UNCHECKABLE (body unreadable).",
536
+ "inputSchema": {
537
+ "type": "object",
538
+ "properties": {},
539
+ "additionalProperties": false
540
+ },
541
+ "outputSchema": {
542
+ "type": "object",
543
+ "properties": {
544
+ "state": {
545
+ "type": "string",
546
+ "description": "VALID or UNREACHABLE"
547
+ },
548
+ "counts": {
549
+ "type": [
550
+ "object",
551
+ "null"
552
+ ]
134
553
  },
135
554
  "as_of": {
555
+ "type": [
556
+ "string",
557
+ "null"
558
+ ]
559
+ },
560
+ "partial": {
561
+ "type": "boolean",
562
+ "description": "true whenever the enumeration did not complete \u2014 a cap-limited read is partial"
563
+ },
564
+ "partial_reason": {
565
+ "type": [
566
+ "string",
567
+ "null"
568
+ ]
569
+ },
570
+ "enumeration": {
571
+ "type": [
572
+ "object",
573
+ "null"
574
+ ]
575
+ },
576
+ "diff": {},
577
+ "kind": {},
578
+ "source": {
136
579
  "type": "string"
580
+ },
581
+ "headline": {
582
+ "type": [
583
+ "string",
584
+ "null"
585
+ ]
137
586
  }
138
- }
587
+ },
588
+ "required": [
589
+ "state"
590
+ ]
591
+ },
592
+ "annotations": {
593
+ "title": "MCP handshake census snapshot",
594
+ "readOnlyHint": true,
595
+ "destructiveHint": false,
596
+ "idempotentHint": true,
597
+ "openWorldHint": true
598
+ }
599
+ },
600
+ {
601
+ "name": "measurement_index",
602
+ "title": "Measurement-capsule index",
603
+ "description": "Read the latest signed measurement-capsule index published at https://councilof.ai/measurement-capsules/latest.json: the index root over every capsule, each batch (adapter, kind, capsule count, measurement states, batch Merkle root, record sha256, record signature and OpenTimestamps state), the index's own board signature re-verified here against the pinned did:web:csoai.org#board-attestation-1 key, and the anchor states published beside it (OpenTimestamps, Rekor, XRPL) \u2014 PENDING is never called attested. States only: measurement, not endorsement; no verdict, score or ranking. NOT_PUBLISHED when no index is served; UNREACHABLE when the source could not be fetched.",
604
+ "annotations": {
605
+ "title": "Measurement-capsule index",
606
+ "readOnlyHint": true,
607
+ "destructiveHint": false,
608
+ "idempotentHint": true,
609
+ "openWorldHint": true
610
+ },
611
+ "inputSchema": {
612
+ "type": "object",
613
+ "properties": {},
614
+ "additionalProperties": false
615
+ },
616
+ "outputSchema": {
617
+ "type": "object",
618
+ "properties": {
619
+ "state": {
620
+ "type": "string",
621
+ "enum": [
622
+ "PUBLISHED",
623
+ "NOT_PUBLISHED",
624
+ "UNREACHABLE",
625
+ "UNCHECKABLE"
626
+ ]
627
+ },
628
+ "doctrine": {
629
+ "type": "string",
630
+ "const": "measurement, not endorsement"
631
+ },
632
+ "version": {
633
+ "type": "string"
634
+ },
635
+ "index_root": {
636
+ "type": [
637
+ "string",
638
+ "null"
639
+ ]
640
+ },
641
+ "n_capsules_total": {
642
+ "type": [
643
+ "integer",
644
+ "null"
645
+ ]
646
+ },
647
+ "n_batches": {
648
+ "type": "integer"
649
+ },
650
+ "batches": {
651
+ "type": "array",
652
+ "items": {
653
+ "type": "object"
654
+ }
655
+ },
656
+ "signature": {
657
+ "type": "object"
658
+ },
659
+ "anchors": {
660
+ "type": "object"
661
+ },
662
+ "reason": {
663
+ "type": "string"
664
+ }
665
+ },
666
+ "required": [
667
+ "state"
668
+ ]
669
+ }
670
+ },
671
+ {
672
+ "name": "verify_capsule",
673
+ "title": "Verify one measurement capsule",
674
+ "description": "Verify one measurement capsule. Pass capsule_json as the capsule's JSON TEXT (exact: number lexemes are kept) or as an object. Recomputes capsule_id (sha256 of the canonical JSON without capsule_id), picks the rule by the capsule's schema (csoai.measurement-capsule/0.2: RFC 6962 Merkle with 0x00/0x01 domain separation; the superseded v0.1 capsule schema: the v0.1 rule), finds the published batch of the same kind, recomputes that batch's root from its published leaves, and returns the audit path of this capsule against the root named by the signed index. States: INCLUDED, NOT_INCLUDED, ID_MISMATCH, UNCHECKABLE, NOT_PUBLISHED. Verifying is free forever. It proves the bytes were published and bound; what they measured is the capsule's own measurement_state and limitations \u2014 measurement, not endorsement.",
675
+ "annotations": {
676
+ "title": "Verify one measurement capsule",
677
+ "readOnlyHint": true,
678
+ "destructiveHint": false,
679
+ "idempotentHint": true,
680
+ "openWorldHint": true
681
+ },
682
+ "inputSchema": {
683
+ "type": "object",
684
+ "properties": {
685
+ "capsule_json": {
686
+ "description": "The capsule: its JSON text (preferred, byte-exact) or the parsed object.",
687
+ "type": [
688
+ "string",
689
+ "object"
690
+ ]
691
+ }
692
+ },
693
+ "required": [
694
+ "capsule_json"
695
+ ],
696
+ "additionalProperties": false
697
+ },
698
+ "outputSchema": {
699
+ "type": "object",
700
+ "properties": {
701
+ "state": {
702
+ "type": "string",
703
+ "enum": [
704
+ "INCLUDED",
705
+ "NOT_INCLUDED",
706
+ "ID_MISMATCH",
707
+ "UNCHECKABLE",
708
+ "NOT_PUBLISHED",
709
+ "UNREACHABLE"
710
+ ]
711
+ },
712
+ "doctrine": {
713
+ "type": "string",
714
+ "const": "measurement, not endorsement"
715
+ },
716
+ "capsule_id": {
717
+ "type": "object"
718
+ },
719
+ "batch": {
720
+ "type": "object"
721
+ },
722
+ "inclusion": {
723
+ "type": "object"
724
+ },
725
+ "index_signature": {
726
+ "type": "object"
727
+ },
728
+ "reason": {
729
+ "type": "string"
730
+ }
731
+ },
732
+ "required": [
733
+ "state"
734
+ ]
735
+ }
736
+ },
737
+ {
738
+ "name": "server_evidence",
739
+ "title": "Every published capsule about one endpoint",
740
+ "description": "Trust per server, not totals: every published measurement capsule about ONE endpoint URL across all batches \u2014 MCP contract-parity dimensions (AUTH, PAYMENT, PROTOCOL, TOOLS, VERSION), A2A card-signature state, self-parity cells for CSOAI's own doors, and any later adapter (e.g. tool drift) \u2014 each with its measurement_state, observed_at, correction_pointer, limitations, batch Merkle root and an inclusion pointer (verify_capsule re-derives inclusion). Read from a static per-endpoint shard keyed by sha256 of the normalised URL. An endpoint with no capsule answers NOT_MEASURED with an empty list \u2014 never an error and never a clean bill. No verdict, score or ranking: measurement, not endorsement.",
741
+ "annotations": {
742
+ "title": "Every published capsule about one endpoint",
743
+ "readOnlyHint": true,
744
+ "destructiveHint": false,
745
+ "idempotentHint": true,
746
+ "openWorldHint": true
747
+ },
748
+ "inputSchema": {
749
+ "type": "object",
750
+ "properties": {
751
+ "endpoint_url": {
752
+ "type": "string",
753
+ "description": "The endpoint URL, e.g. https://example.com/mcp or an agent-card URL."
754
+ }
755
+ },
756
+ "required": [
757
+ "endpoint_url"
758
+ ],
759
+ "additionalProperties": false
760
+ },
761
+ "outputSchema": {
762
+ "type": "object",
763
+ "properties": {
764
+ "state": {
765
+ "type": "string",
766
+ "enum": [
767
+ "MEASURED",
768
+ "NOT_MEASURED",
769
+ "NOT_PUBLISHED",
770
+ "UNREACHABLE",
771
+ "UNCHECKABLE"
772
+ ]
773
+ },
774
+ "doctrine": {
775
+ "type": "string",
776
+ "const": "measurement, not endorsement"
777
+ },
778
+ "endpoint": {
779
+ "type": [
780
+ "string",
781
+ "null"
782
+ ]
783
+ },
784
+ "n_capsules": {
785
+ "type": "integer"
786
+ },
787
+ "capsules": {
788
+ "type": "array",
789
+ "items": {
790
+ "type": "object"
791
+ }
792
+ },
793
+ "reason": {
794
+ "type": "string"
795
+ }
796
+ },
797
+ "required": [
798
+ "state"
799
+ ]
139
800
  }
140
801
  }
141
802
  ]
package/index.mjs CHANGED
@@ -79,6 +79,26 @@ const PAID_TOOLS = PAID_TOOLS_PATH ? JSON.parse(readFileSync(PAID_TOOLS_PATH, "u
79
79
  const PAID_BY_NAME = new Map(PAID_TOOLS.map((t) => [t.name, t]));
80
80
  const TOOLS = [...FREE_TOOLS, ...PAID_TOOLS];
81
81
 
82
+ /**
83
+ * Axis names: ONE alias table shared with the HTTP door (functions/mcp/axis-aliases.json) and the
84
+ * Python client. `governance`, `GOV` and `gspc-governance` name the same axis on every surface.
85
+ */
86
+ const ALIASES_PATH = firstExisting([
87
+ "../../functions/mcp/axis-aliases.json", // repo checkout: the canonical file
88
+ "./axis-aliases.json", // npm package: the byte-identical pack-time copy
89
+ ]);
90
+ const AXIS_TABLE = ALIASES_PATH ? JSON.parse(readFileSync(ALIASES_PATH, "utf8")).axes : {};
91
+ const AXIS_CANON = new Map();
92
+ for (const [canonical, aliases] of Object.entries(AXIS_TABLE)) {
93
+ AXIS_CANON.set(canonical.toLowerCase(), canonical);
94
+ for (const a of aliases) AXIS_CANON.set(String(a).toLowerCase(), canonical);
95
+ }
96
+ function canonicalAxis(name) {
97
+ const k = String(name ?? "").trim().toLowerCase();
98
+ return AXIS_CANON.get(k) ?? k;
99
+ }
100
+ const sameAxis = (a, b) => canonicalAxis(a) === canonicalAxis(b);
101
+
82
102
  const VERIFIER_PATH = firstExisting([
83
103
  "../../public/signed/verify-card.mjs", // repo checkout: the canonical file
84
104
  "./verify-card.mjs", // npm package: the byte-identical pack-time copy
@@ -163,8 +183,9 @@ async function boardTotals() {
163
183
  }
164
184
 
165
185
  async function getAxis(args) {
166
- const wanted = String(args.axis ?? "").trim().toLowerCase();
167
- if (!wanted) return { state: "BAD_INPUT", error: "pass an axis name, e.g. governance" };
186
+ const asked = String(args.axis ?? "").trim();
187
+ if (!asked) return { state: "BAD_INPUT", error: "pass an axis name, e.g. governance" };
188
+ const wanted = canonicalAxis(asked);
168
189
  let d;
169
190
  try {
170
191
  d = await fetchJson("/api/gspc");
@@ -172,11 +193,11 @@ async function getAxis(args) {
172
193
  return unreachable("/api/gspc", e);
173
194
  }
174
195
  const rows = d.axes ?? [];
175
- const row = rows.find((r) => String(r.axis ?? "").toLowerCase() === wanted);
196
+ const row = rows.find((r) => sameAxis(r.axis, wanted));
176
197
  if (!row) {
177
198
  return {
178
199
  state: "NOT_ON_BOARD",
179
- axis: wanted,
200
+ axis: asked,
180
201
  note: "This name is not a row on the live board. That is a fact about the board, not a verdict about the subject.",
181
202
  board_carries: rows.map((r) => r.axis),
182
203
  as_of: { board_measured_on: d.measured_on ?? null, fetched_at: new Date().toISOString() },
@@ -186,6 +207,7 @@ async function getAxis(args) {
186
207
  return {
187
208
  state: "LIVE",
188
209
  axis: row.axis,
210
+ ...(asked.toLowerCase() !== String(row.axis).toLowerCase() ? { resolved_from: asked } : {}),
189
211
  family: row.family ?? null,
190
212
  status: row.status ?? null,
191
213
  measured,
@@ -197,6 +219,13 @@ async function getAxis(args) {
197
219
  interval: row.interval ?? null,
198
220
  leader: row.leader ?? null,
199
221
  dataset: row.dataset ?? null,
222
+ // Same three door fields the HTTP door returns (functions/mcp/_board.ts): copied from the row,
223
+ // null when the row carries none, never invented.
224
+ kind: row.kind ?? null,
225
+ dataset_url: row.dataset_url ?? null,
226
+ evidence_url: typeof row.evidence_url === "string" && row.evidence_url.startsWith("/")
227
+ ? `${ORIGIN}${row.evidence_url}` : (row.evidence_url ?? null),
228
+ row_url: `${ORIGIN}/api/gspc?axis=${encodeURIComponent(String(row.axis))}`,
200
229
  note: row.note ?? null,
201
230
  as_of: { board_measured_on: d.measured_on ?? null, fetched_at: new Date().toISOString() },
202
231
  source: `${ORIGIN}/api/gspc`,
@@ -272,10 +301,20 @@ async function listCards(args) {
272
301
  packaged_at: idx.packaged_at ?? null,
273
302
  pubkey: idx.pubkey ?? null,
274
303
  };
275
- const wanted = args.axis ? String(args.axis).toLowerCase() : null;
304
+ const wanted = args.axis ? canonicalAxis(args.axis) : null;
276
305
  const limit = Number.isInteger(args.limit) ? args.limit : 10;
306
+ if (wanted) {
307
+ const matched = [...new Set(rows.map((r) => String(r.axis ?? "")).filter((a) => sameAxis(a, wanted)))].sort();
308
+ out.axis_query = {
309
+ asked: String(args.axis),
310
+ canonical: wanted,
311
+ spellings: AXIS_TABLE[wanted] ? [wanted, ...AXIS_TABLE[wanted]] : [wanted],
312
+ index_names_matched: matched,
313
+ note: "rows whose index axis name resolves to the same axis under functions/mcp/axis-aliases.json; each row keeps the index's own spelling",
314
+ };
315
+ }
277
316
  out.rows = rows
278
- .filter((r) => !wanted || String(r.axis ?? "").toLowerCase() === wanted)
317
+ .filter((r) => !wanted || sameAxis(r.axis, wanted))
279
318
  .slice()
280
319
  .sort((a, b) => String(b.ts ?? "").localeCompare(String(a.ts ?? "")))
281
320
  .slice(0, limit)
@@ -376,6 +415,44 @@ async function verifyInclusion(args) {
376
415
  * canonical measured artefact; this package delegates to it instead of
377
416
  * copying counts or manufacturing a trust verdict locally.
378
417
  */
418
+ /** partial = the snapshot says so OR its enumeration did not complete (a cap is not completion). Mirrors functions/mcp/_board.ts partialOf. */
419
+ function partialOf(d) {
420
+ const e = d.enumeration ?? {};
421
+ if (d.partial === true) return { partial: true, partial_reason: String(e.stop_reason ?? "the snapshot marks itself partial") };
422
+ if (e.complete === false) {
423
+ const seen = e.rows_with_remote ?? e.registry_rows_seen;
424
+ return {
425
+ partial: true,
426
+ partial_reason: `enumeration incomplete: ${String(e.stop_reason ?? "stopped early")}${
427
+ e.unique_hosts != null && seen != null ? ` (${e.unique_hosts} hosts probed of ${seen} registry rows with a remote)` : ""
428
+ }`,
429
+ };
430
+ }
431
+ return { partial: false, partial_reason: null };
432
+ }
433
+
434
+ async function mcpTrust() {
435
+ const path = "/interop/mcp-trust/latest.json";
436
+ try {
437
+ const d = await fetchJson(path);
438
+ return {
439
+ state: "VALID",
440
+ source: `${ORIGIN}${path}`,
441
+ kind: d.kind ?? null,
442
+ as_of: d.as_of ?? null,
443
+ // A cap-limited read is PARTIAL — read from the enumeration, not only the flag (2026-09-26).
444
+ ...partialOf(d),
445
+ enumeration: d.enumeration ?? null,
446
+ counts: d.counts ?? null,
447
+ headline: d.headline ?? null,
448
+ diff: d.diff ?? null,
449
+ not_a_certification: true,
450
+ };
451
+ } catch (e) {
452
+ return { ...unreachable(path, e), state: "UNREACHABLE" };
453
+ }
454
+ }
455
+
379
456
  async function x402Trust() {
380
457
  const path = "/interop/x402-trust/latest.json";
381
458
  try {
@@ -394,6 +471,56 @@ async function x402Trust() {
394
471
  }
395
472
  }
396
473
 
474
+ /* ------------------------------------------------- measurement-capsule readers */
475
+
476
+ /**
477
+ * measurement_index, verify_capsule and server_evidence are answered by the door's own /mcp
478
+ * (functions/_lib/measurementCapsule.ts): this package forwards the call and returns the door's
479
+ * structuredContent unchanged, so the two implementations cannot disagree about a capsule. The door
480
+ * re-derives everything it returns (capsule ids, Merkle inclusion, the index signature against the
481
+ * pinned key). If the door cannot be reached the answer is UNREACHABLE — never a guess, never
482
+ * NOT_MEASURED (which is a statement about the index, not about the connection).
483
+ */
484
+ const MEASUREMENT_DOCTRINE = "measurement, not endorsement";
485
+
486
+ async function doorTool(name, args) {
487
+ const url = `${ORIGIN}/mcp`;
488
+ try {
489
+ const r = await fetch(url, {
490
+ method: "POST",
491
+ headers: {
492
+ "content-type": "application/json",
493
+ accept: "application/json, text/event-stream",
494
+ "MCP-Protocol-Version": "2025-03-26",
495
+ },
496
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name, arguments: args } }),
497
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
498
+ });
499
+ if (!r.ok) throw new Error(`POST ${url} returned HTTP ${r.status}`);
500
+ const text = await r.text();
501
+ const ct = r.headers.get("content-type") || "";
502
+ let msg;
503
+ if (ct.includes("text/event-stream")) {
504
+ const frames = text
505
+ .replace(/\r\n/g, "\n")
506
+ .split(/\n\n/)
507
+ .map((e) => e.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).join("\n"))
508
+ .filter(Boolean);
509
+ msg = JSON.parse(frames[frames.length - 1]);
510
+ } else msg = JSON.parse(text);
511
+ const sc = msg?.result?.structuredContent;
512
+ if (!sc || typeof sc !== "object") throw new Error(msg?.error?.message || "the door returned no structuredContent");
513
+ return { ...sc, answered_by: url };
514
+ } catch (e) {
515
+ return {
516
+ state: "UNREACHABLE",
517
+ doctrine: MEASUREMENT_DOCTRINE,
518
+ reason: `the door that answers ${name} could not be reached: ${e instanceof Error ? e.message : String(e)}`,
519
+ source: url,
520
+ };
521
+ }
522
+ }
523
+
397
524
  /* ---------------------------------------------------------------- paid tools */
398
525
 
399
526
  const PAID_DOCTRINE =
@@ -632,11 +759,19 @@ const HANDLERS = {
632
759
  get_card: getCard,
633
760
  verify_inclusion: verifyInclusion,
634
761
  x402_trust: x402Trust,
762
+ mcp_trust: mcpTrust,
763
+ measurement_index: (a) => doorTool("measurement_index", a),
764
+ verify_capsule: (a) => doorTool("verify_capsule", a),
765
+ server_evidence: (a) => doorTool("server_evidence", a),
635
766
  };
636
767
 
637
768
  /* ----------------------------------------------------------------- transport */
638
769
 
639
- const SUPPORTED_PROTOCOLS = ["2024-11-05", "2025-03-26", "2025-06-18"];
770
+ // Oldest first; the LAST entry is the latest this server speaks. An unknown requested version is
771
+ // answered with the latest (MCP lifecycle: "the server MUST respond with another protocol version
772
+ // it supports. This SHOULD be the latest version supported"), not the oldest as it was until
773
+ // 2026-09-26. README.md "stdio" lists exactly this array (tools-match-door.test.ts checks it).
774
+ const SUPPORTED_PROTOCOLS = ["2024-11-05", "2025-03-26", "2025-06-18", "2025-11-25"];
640
775
 
641
776
  function send(msg) {
642
777
  process.stdout.write(JSON.stringify(msg) + "\n");
@@ -675,6 +810,12 @@ function summaryLine(name, payload) {
675
810
  return `${payload.state ?? "?"} — inclusion against live merkle.`;
676
811
  case "x402_trust":
677
812
  return `${payload.state ?? "?"} — ${payload.headline || "catalog trust counts"}.`;
813
+ case "mcp_trust":
814
+ return `${payload.state ?? "?"} — MCP handshake census${payload.partial ? " (partial round)" : ""}.`;
815
+ case "measurement_index":
816
+ case "verify_capsule":
817
+ case "server_evidence":
818
+ return `${payload.state ?? "?"}${payload.reason ? " — " + payload.reason : ""} (${MEASUREMENT_DOCTRINE}).`;
678
819
  case "commission_card":
679
820
  case "art50_marking_evidence":
680
821
  case "rwa_evidence":
@@ -694,7 +835,7 @@ async function handle(msg) {
694
835
  if (method === "initialize") {
695
836
  const asked = params?.protocolVersion;
696
837
  return reply(id, {
697
- protocolVersion: SUPPORTED_PROTOCOLS.includes(asked) ? asked : "2024-11-05",
838
+ protocolVersion: SUPPORTED_PROTOCOLS.includes(asked) ? asked : SUPPORTED_PROTOCOLS[SUPPORTED_PROTOCOLS.length - 1],
698
839
  capabilities: { tools: {} },
699
840
  serverInfo: { name: "csoai-gspc-mcp", version: VERSION },
700
841
  });
@@ -709,6 +850,24 @@ async function handle(msg) {
709
850
  if (!fn) return replyError(id, -32602, `unknown tool: ${name}`);
710
851
  try {
711
852
  const payload = await fn(params?.arguments ?? {});
853
+ // x402 MCP transport (x402-foundation/x402 specs/transports-v2/mcp.md): a payment challenge is
854
+ // a tool result with isError:true whose structuredContent IS the PaymentRequired object and
855
+ // whose content[0].text is that object as JSON. Same shape as the HTTP door
856
+ // (functions/mcp/_paid.ts). Payment is still read only from the x_payment argument.
857
+ if (payload?.status === "PAYMENT_REQUIRED") {
858
+ const pr = payload.payment_required && typeof payload.payment_required === "object" && !Array.isArray(payload.payment_required)
859
+ ? payload.payment_required
860
+ : {};
861
+ const sc = { ...pr, ...payload };
862
+ return reply(id, {
863
+ content: [
864
+ { type: "text", text: JSON.stringify(sc) },
865
+ { type: "text", text: summaryLine(name, payload) },
866
+ ],
867
+ structuredContent: sc,
868
+ isError: true,
869
+ });
870
+ }
712
871
  return reply(id, {
713
872
  content: [{ type: "text", text: `${summaryLine(name, payload)}\n\n${JSON.stringify(payload, null, 2)}` }],
714
873
  structuredContent: payload,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "csoai-gspc-mcp",
3
- "version": "0.2.2",
4
- "description": "Stdio MCP server for Council of AI's GSPC board and signed measurement evidence. Twelve tools: eight free readers and four x402-metered evidence tools. Measurement only, never certification. Zero runtime dependencies.",
3
+ "version": "0.2.3-rc.2",
4
+ "description": "Stdio MCP server for Council of AI's GSPC board and signed measurement evidence. Sixteen tools: twelve free readers and four x402-metered evidence tools. Measurement only, never certification. Zero runtime dependencies.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "csoai-gspc-mcp": "index.mjs"
@@ -9,6 +9,7 @@
9
9
  "main": "index.mjs",
10
10
  "files": [
11
11
  "README.md",
12
+ "axis-aliases.json",
12
13
  "gspc-tools.json",
13
14
  "index.mjs",
14
15
  "paid-tools.json",
@@ -51,5 +52,5 @@
51
52
  "engines": {
52
53
  "node": ">=20"
53
54
  },
54
- "mcpName": "io.github.CSOAI-ORG/gspc"
55
+ "mcpName": "ai.councilof/gspc"
55
56
  }
package/paid-tools.json CHANGED
@@ -5,6 +5,9 @@
5
5
  "tools": [
6
6
  {
7
7
  "name": "commission_card",
8
+ "title": "Commission a measurement card (paid, x402)",
9
+ "annotations": {"title": "Commission a measurement card (paid, x402)", "readOnlyHint": false, "destructiveHint": false, "idempotentHint": false, "openWorldHint": true},
10
+ "outputSchema": {"type": "object", "description": "The wrapper's status fields. On PAYMENT_REQUIRED (isError:true) the route's x402 PaymentRequired object (x402Version, resource, accepts, extensions) is spread at the top level too, per the x402 MCP transport.", "properties": {"status": {"type": "string", "description": "PAYMENT_REQUIRED, DELIVERED, NOT_DEPLOYED, UNREACHABLE, UNREADABLE_RESPONSE, BAD_ARGUMENTS or HTTP_<code>"}, "tool": {"type": "string"}, "route": {"type": "string"}, "http_status": {"type": "integer"}, "payment_presented": {"type": "boolean"}, "delivery_state": {"type": "string"}, "settlement_state": {"type": "string"}, "x402Version": {"type": "integer"}, "accepts": {"type": "array"}, "resource": {"type": "object"}, "deliverable": {}, "not_a_certification": {"type": "boolean"}}, "required": ["status"]},
8
11
  "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
12
  "inputSchema": {
10
13
  "type": "object",
@@ -20,6 +23,9 @@
20
23
  },
21
24
  {
22
25
  "name": "art50_marking_evidence",
26
+ "title": "Article 50 marking evidence (paid, x402; preview free)",
27
+ "annotations": {"title": "Article 50 marking evidence (paid, x402; preview free)", "readOnlyHint": false, "destructiveHint": false, "idempotentHint": false, "openWorldHint": true},
28
+ "outputSchema": {"type": "object", "description": "The wrapper's status fields. On PAYMENT_REQUIRED (isError:true) the route's x402 PaymentRequired object (x402Version, resource, accepts, extensions) is spread at the top level too, per the x402 MCP transport.", "properties": {"status": {"type": "string", "description": "PAYMENT_REQUIRED, DELIVERED, NOT_DEPLOYED, UNREACHABLE, UNREADABLE_RESPONSE, BAD_ARGUMENTS or HTTP_<code>"}, "tool": {"type": "string"}, "route": {"type": "string"}, "http_status": {"type": "integer"}, "payment_presented": {"type": "boolean"}, "delivery_state": {"type": "string"}, "settlement_state": {"type": "string"}, "x402Version": {"type": "integer"}, "accepts": {"type": "array"}, "resource": {"type": "object"}, "deliverable": {}, "not_a_certification": {"type": "boolean"}}, "required": ["status"]},
23
29
  "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
30
  "inputSchema": {
25
31
  "type": "object",
@@ -36,6 +42,9 @@
36
42
  },
37
43
  {
38
44
  "name": "rwa_evidence",
45
+ "title": "RWA asset evidence (paid, x402; preview free)",
46
+ "annotations": {"title": "RWA asset evidence (paid, x402; preview free)", "readOnlyHint": false, "destructiveHint": false, "idempotentHint": false, "openWorldHint": true},
47
+ "outputSchema": {"type": "object", "description": "The wrapper's status fields. On PAYMENT_REQUIRED (isError:true) the route's x402 PaymentRequired object (x402Version, resource, accepts, extensions) is spread at the top level too, per the x402 MCP transport.", "properties": {"status": {"type": "string", "description": "PAYMENT_REQUIRED, DELIVERED, NOT_DEPLOYED, UNREACHABLE, UNREADABLE_RESPONSE, BAD_ARGUMENTS or HTTP_<code>"}, "tool": {"type": "string"}, "route": {"type": "string"}, "http_status": {"type": "integer"}, "payment_presented": {"type": "boolean"}, "delivery_state": {"type": "string"}, "settlement_state": {"type": "string"}, "x402Version": {"type": "integer"}, "accepts": {"type": "array"}, "resource": {"type": "object"}, "deliverable": {}, "not_a_certification": {"type": "boolean"}}, "required": ["status"]},
39
48
  "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
49
  "inputSchema": {
41
50
  "type": "object",
@@ -51,6 +60,9 @@
51
60
  },
52
61
  {
53
62
  "name": "receipts_batch",
63
+ "title": "Receipts batch (paid, x402; preview free)",
64
+ "annotations": {"title": "Receipts batch (paid, x402; preview free)", "readOnlyHint": false, "destructiveHint": false, "idempotentHint": false, "openWorldHint": true},
65
+ "outputSchema": {"type": "object", "description": "The wrapper's status fields. On PAYMENT_REQUIRED (isError:true) the route's x402 PaymentRequired object (x402Version, resource, accepts, extensions) is spread at the top level too, per the x402 MCP transport.", "properties": {"status": {"type": "string", "description": "PAYMENT_REQUIRED, DELIVERED, NOT_DEPLOYED, UNREACHABLE, UNREADABLE_RESPONSE, BAD_ARGUMENTS or HTTP_<code>"}, "tool": {"type": "string"}, "route": {"type": "string"}, "http_status": {"type": "integer"}, "payment_presented": {"type": "boolean"}, "delivery_state": {"type": "string"}, "settlement_state": {"type": "string"}, "x402Version": {"type": "integer"}, "accepts": {"type": "array"}, "resource": {"type": "object"}, "deliverable": {}, "not_a_certification": {"type": "boolean"}}, "required": ["status"]},
54
66
  "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.",
55
67
  "inputSchema": {
56
68
  "type": "object",