r402-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +133 -0
- package/dist/canonical.d.ts +3 -0
- package/dist/canonical.js +46 -0
- package/dist/canonical.js.map +1 -0
- package/dist/constants.d.ts +8 -0
- package/dist/constants.js +11 -0
- package/dist/constants.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +85 -0
- package/dist/server.js.map +1 -0
- package/dist/signers.d.ts +49 -0
- package/dist/signers.js +52 -0
- package/dist/signers.js.map +1 -0
- package/dist/verify.d.ts +34 -0
- package/dist/verify.js +92 -0
- package/dist/verify.js.map +1 -0
- package/examples/demo-anchor.json +26 -0
- package/examples/verify-demo.md +43 -0
- package/package.json +37 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 rsynthlabs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# r402-mcp
|
|
2
|
+
|
|
3
|
+
mcp server for [r402](https://github.com/rsynthlabs/r402) · verify $R execution proofs anchored on base mainnet · local: recompute the payload hash, recover the signer, and read the ExecutionLog contract over public rpc
|
|
4
|
+
|
|
5
|
+
two tools: `verify_execution` (free, local verify) · `query_signers` (free, direct rpc).
|
|
6
|
+
|
|
7
|
+
## install
|
|
8
|
+
|
|
9
|
+
claude code:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
claude mcp add r402 -- npx -y r402-mcp
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
claude desktop (`claude_desktop_config.json`):
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"mcpServers": {
|
|
20
|
+
"r402": {
|
|
21
|
+
"command": "npx",
|
|
22
|
+
"args": ["-y", "r402-mcp"]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## environment
|
|
29
|
+
|
|
30
|
+
| var | required | what |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `BASE_RPC_URL` | no | base mainnet rpc for `verify_execution` and `query_signers`. default `https://mainnet.base.org` |
|
|
33
|
+
|
|
34
|
+
no private key, no payment. the server never reads `.env` by itself. env comes from the mcp client config, or `node --env-file=.env` for local runs.
|
|
35
|
+
|
|
36
|
+
## tools
|
|
37
|
+
|
|
38
|
+
### verify_execution
|
|
39
|
+
|
|
40
|
+
price: free. verifies an execution proof locally and never relays a remote verdict:
|
|
41
|
+
|
|
42
|
+
1. recompute `keccak256(canonical_bytes(payload))` (SCHEMA.md §2 canonicalization).
|
|
43
|
+
2. recover the eip-191 signer from `signature` over that hash.
|
|
44
|
+
3. read the `ExecutionLog` anchor (`0xd5A9DAF8F2134b61b73cEfaF5c9094EA162f1a1c`) for `txHash` over rpc and decode `ExecutionRecorded(signer, payloadHash)`.
|
|
45
|
+
4. `verified` is true only when the recomputed hash equals the anchored hash and the recovered signer equals the anchored signer.
|
|
46
|
+
|
|
47
|
+
params:
|
|
48
|
+
- `payload` · the off-chain execution payload json (v0.1 schema) that was signed and anchored
|
|
49
|
+
- `signature` · 0x-prefixed 65-byte eip-191 signature over the payload hash
|
|
50
|
+
- `txHash` · 0x-prefixed 32-byte tx hash of the anchor on base
|
|
51
|
+
|
|
52
|
+
a complete demo triple lives in [`examples/demo-anchor.json`](examples/demo-anchor.json) (verify it yourself: [`examples/verify-demo.md`](examples/verify-demo.md)).
|
|
53
|
+
|
|
54
|
+
<!-- demo-anchor:input:begin -->
|
|
55
|
+
```
|
|
56
|
+
verify_execution {
|
|
57
|
+
"payload": {
|
|
58
|
+
"version": "0.1.0",
|
|
59
|
+
"agent_id": 20617,
|
|
60
|
+
"robot_id": "sim-lerobot-pusht-ep1",
|
|
61
|
+
"episode_id": "ep_2026-06-19T15-04-10Z_pusht1",
|
|
62
|
+
"task": "push T to goal (lerobot/pusht reference replay)",
|
|
63
|
+
"started_at": "2026-06-19T15:04:10Z",
|
|
64
|
+
"ended_at": "2026-06-19T15:04:28Z",
|
|
65
|
+
"duration_seconds": 18.4,
|
|
66
|
+
"frames": 521,
|
|
67
|
+
"metrics": {
|
|
68
|
+
"rmse": 3.214,
|
|
69
|
+
"jerk": 1581043,
|
|
70
|
+
"end_variance": 0
|
|
71
|
+
},
|
|
72
|
+
"score": 0.88,
|
|
73
|
+
"outcome": "SUCCESS"
|
|
74
|
+
},
|
|
75
|
+
"signature": "0x30b36f9ec1bbb18a40247e52568a41c9c424adec7c5209cc7a4da9b47ca35be645b1243a430bf63845d1eb12858b321f2924b5264371a543358ba29f9a8d5c911b",
|
|
76
|
+
"txHash": "0xee10ef6a112f05d9d3761aa055ab258f1afffd54f8c02212366e96a1da0915bb"
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
<!-- demo-anchor:input:end -->
|
|
80
|
+
|
|
81
|
+
verified (recomputed hash and recovered signer both match the anchor):
|
|
82
|
+
|
|
83
|
+
<!-- demo-anchor:result:begin -->
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"verified": true,
|
|
87
|
+
"signer": "0x82960f3322a7B1d2a2e756Efcbd4d1D56B613314",
|
|
88
|
+
"payloadHash": "0x1d646db564305a51e5e617245a54bd6fc3bd457cb82488397ba38e1e1c559f26",
|
|
89
|
+
"signature": "0x30b36f9ec1bbb18a40247e52568a41c9c424adec7c5209cc7a4da9b47ca35be645b1243a430bf63845d1eb12858b321f2924b5264371a543358ba29f9a8d5c911b",
|
|
90
|
+
"block": 47568429,
|
|
91
|
+
"timestamp": 1781926205,
|
|
92
|
+
"txHash": "0xee10ef6a112f05d9d3761aa055ab258f1afffd54f8c02212366e96a1da0915bb"
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
<!-- demo-anchor:result:end -->
|
|
96
|
+
|
|
97
|
+
not verified (hash mismatch, signer mismatch, no anchor event in the tx, or reverted tx):
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{ "verified": false, "reason": "recovered signer does not match the on-chain anchor" }
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### query_signers
|
|
104
|
+
|
|
105
|
+
price: free. reads `ExecutionRecorded` events of the executionlog contract (`0xd5A9DAF8F2134b61b73cEfaF5c9094EA162f1a1c`) directly over rpc, from deploy block to tip.
|
|
106
|
+
|
|
107
|
+
params: `limit` · max signers to return, 1-100, default 50
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
query_signers { "limit": 10 }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"signers": [
|
|
116
|
+
{ "address": "0x156d727f372d06132526612b7d34ce1693365bf3", "anchor_count": 8, "first_seen_block": 46166512 }
|
|
117
|
+
],
|
|
118
|
+
"total_anchors": 14
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## dev
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
pnpm install
|
|
126
|
+
pnpm test
|
|
127
|
+
pnpm build
|
|
128
|
+
node --env-file=.env dist/server.js
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## license
|
|
132
|
+
|
|
133
|
+
mit
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { keccak256, stringToBytes } from 'viem';
|
|
2
|
+
// float-typed leaves of the v0.1 execution-payload schema (SCHEMA.md §1). js has no
|
|
3
|
+
// int/float distinction, so to reproduce python's json.dumps bytes these fields keep a
|
|
4
|
+
// trailing .0 when integer-valued (2434753 -> 2434753.0, 0 -> 0.0); int fields
|
|
5
|
+
// (agent_id, frames) must not. schema-aware, not generic. see SCHEMA.md §2.
|
|
6
|
+
const FLOAT_FIELDS = new Set([
|
|
7
|
+
'duration_seconds',
|
|
8
|
+
'score',
|
|
9
|
+
'metrics.rmse',
|
|
10
|
+
'metrics.jerk',
|
|
11
|
+
'metrics.end_variance',
|
|
12
|
+
]);
|
|
13
|
+
function encode(value, path) {
|
|
14
|
+
if (value === null)
|
|
15
|
+
return 'null';
|
|
16
|
+
if (typeof value === 'string')
|
|
17
|
+
return JSON.stringify(value);
|
|
18
|
+
if (typeof value === 'boolean')
|
|
19
|
+
return value ? 'true' : 'false';
|
|
20
|
+
if (typeof value === 'number') {
|
|
21
|
+
let s = String(value);
|
|
22
|
+
if (FLOAT_FIELDS.has(path) && !/[.eE]/.test(s))
|
|
23
|
+
s += '.0';
|
|
24
|
+
return s;
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(value))
|
|
27
|
+
return '[' + value.map((v) => encode(v, path)).join(',') + ']';
|
|
28
|
+
const keys = Object.keys(value)
|
|
29
|
+
// additive v0.2: drop an unset policy block so v0.1 payloads hash identically.
|
|
30
|
+
.filter((k) => !(k === 'policy' && value[k] === null))
|
|
31
|
+
.sort();
|
|
32
|
+
return ('{' +
|
|
33
|
+
keys
|
|
34
|
+
.map((k) => JSON.stringify(k) + ':' + encode(value[k], path ? `${path}.${k}` : k))
|
|
35
|
+
.join(',') +
|
|
36
|
+
'}');
|
|
37
|
+
}
|
|
38
|
+
// SCHEMA.md §2: sorted keys at every depth, no whitespace, utf-8, no trailing newline.
|
|
39
|
+
// byte-identical to rsynth.payload.canonical_bytes.
|
|
40
|
+
export function canonicalize(payload) {
|
|
41
|
+
return encode(payload, '');
|
|
42
|
+
}
|
|
43
|
+
export function payloadHash(payload) {
|
|
44
|
+
return keccak256(stringToBytes(canonicalize(payload)));
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=canonical.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canonical.js","sourceRoot":"","sources":["../src/canonical.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,aAAa,EAAY,MAAM,MAAM,CAAC;AAE1D,oFAAoF;AACpF,uFAAuF;AACvF,+EAA+E;AAC/E,4EAA4E;AAC5E,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC;IAC3B,kBAAkB;IAClB,OAAO;IACP,cAAc;IACd,cAAc;IACd,sBAAsB;CACvB,CAAC,CAAC;AAIH,SAAS,MAAM,CAAC,KAAW,EAAE,IAAY;IACvC,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,MAAM,CAAC;IAClC,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC5D,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;IAChE,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,IAAI,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;QACtB,IAAI,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;YAAE,CAAC,IAAI,IAAI,CAAC;QAC1D,OAAO,CAAC,CAAC;IACX,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;IACzF,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC;QAC7B,+EAA+E;SAC9E,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,QAAQ,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;SACrD,IAAI,EAAE,CAAC;IACV,OAAO,CACL,GAAG;QACH,IAAI;aACD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;aACjF,IAAI,CAAC,GAAG,CAAC;QACZ,GAAG,CACJ,CAAC;AACJ,CAAC;AAED,uFAAuF;AACvF,oDAAoD;AACpD,MAAM,UAAU,YAAY,CAAC,OAAgC;IAC3D,OAAO,MAAM,CAAC,OAAe,EAAE,EAAE,CAAC,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,OAAgC;IAC1D,OAAO,SAAS,CAAC,aAAa,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AACzD,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const SERVER_NAME = "r402-mcp";
|
|
2
|
+
export declare const SERVER_VERSION = "0.1.0";
|
|
3
|
+
export declare const TX_HASH_RE: RegExp;
|
|
4
|
+
export declare const SIGNATURE_RE: RegExp;
|
|
5
|
+
export declare const EXECUTION_LOG_ADDRESS: "0xd5A9DAF8F2134b61b73cEfaF5c9094EA162f1a1c";
|
|
6
|
+
export declare const EXECUTION_LOG_DEPLOY_BLOCK = 46166486n;
|
|
7
|
+
export declare const GET_LOGS_CHUNK_BLOCKS = 10000n;
|
|
8
|
+
export declare const DEFAULT_BASE_RPC_URL = "https://mainnet.base.org";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const SERVER_NAME = 'r402-mcp';
|
|
2
|
+
export const SERVER_VERSION = '0.1.0';
|
|
3
|
+
export const TX_HASH_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
4
|
+
// 65-byte (r, s, v) eip-191 signature.
|
|
5
|
+
export const SIGNATURE_RE = /^0x[0-9a-fA-F]{130}$/;
|
|
6
|
+
export const EXECUTION_LOG_ADDRESS = '0xd5A9DAF8F2134b61b73cEfaF5c9094EA162f1a1c';
|
|
7
|
+
export const EXECUTION_LOG_DEPLOY_BLOCK = 46166486n;
|
|
8
|
+
// public base rpcs cap eth_getLogs ranges around 10k blocks.
|
|
9
|
+
export const GET_LOGS_CHUNK_BLOCKS = 10000n;
|
|
10
|
+
export const DEFAULT_BASE_RPC_URL = 'https://mainnet.base.org';
|
|
11
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC;AACtC,MAAM,CAAC,MAAM,cAAc,GAAG,OAAO,CAAC;AAEtC,MAAM,CAAC,MAAM,UAAU,GAAG,qBAAqB,CAAC;AAChD,uCAAuC;AACvC,MAAM,CAAC,MAAM,YAAY,GAAG,sBAAsB,CAAC;AAEnD,MAAM,CAAC,MAAM,qBAAqB,GAAG,4CAAqD,CAAC;AAC3F,MAAM,CAAC,MAAM,0BAA0B,GAAG,SAAW,CAAC;AACtD,6DAA6D;AAC7D,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAO,CAAC;AAC7C,MAAM,CAAC,MAAM,oBAAoB,GAAG,0BAA0B,CAAC"}
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { DEFAULT_BASE_RPC_URL, SERVER_NAME, SERVER_VERSION, SIGNATURE_RE, TX_HASH_RE, } from './constants.js';
|
|
6
|
+
import { verifyExecution } from './verify.js';
|
|
7
|
+
import { querySigners } from './signers.js';
|
|
8
|
+
const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
|
|
9
|
+
function ok(result) {
|
|
10
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
11
|
+
}
|
|
12
|
+
function fail(err) {
|
|
13
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
14
|
+
return { content: [{ type: 'text', text: message }], isError: true };
|
|
15
|
+
}
|
|
16
|
+
const PAYLOAD_SCHEMA = z.object({
|
|
17
|
+
version: z.string(),
|
|
18
|
+
agent_id: z.number().int(),
|
|
19
|
+
robot_id: z.string(),
|
|
20
|
+
episode_id: z.string(),
|
|
21
|
+
task: z.string(),
|
|
22
|
+
started_at: z.string(),
|
|
23
|
+
ended_at: z.string(),
|
|
24
|
+
duration_seconds: z.number(),
|
|
25
|
+
frames: z.number().int(),
|
|
26
|
+
metrics: z.object({
|
|
27
|
+
rmse: z.number(),
|
|
28
|
+
jerk: z.number(),
|
|
29
|
+
end_variance: z.number(),
|
|
30
|
+
}),
|
|
31
|
+
score: z.number().min(0).max(1),
|
|
32
|
+
outcome: z.enum(['SUCCESS', 'PARTIAL', 'FAIL']),
|
|
33
|
+
});
|
|
34
|
+
server.registerTool('verify_execution', {
|
|
35
|
+
description: 'verify an r402 execution proof locally: recompute keccak256 of the canonical payload, recover the eip-191 signer from the signature, and read the executionlog anchor (0xd5A9DAF8F2134b61b73cEfaF5c9094EA162f1a1c) on base by tx hash over public rpc. free, no payment, no relayed verdict. returns verified true only when the recomputed hash and recovered signer both match the on-chain anchor.',
|
|
36
|
+
inputSchema: {
|
|
37
|
+
payload: PAYLOAD_SCHEMA.describe('the off-chain execution payload json (v0.1 schema) that was signed and anchored'),
|
|
38
|
+
signature: z
|
|
39
|
+
.string()
|
|
40
|
+
.regex(SIGNATURE_RE)
|
|
41
|
+
.describe('0x-prefixed 65-byte eip-191 signature over the payload hash'),
|
|
42
|
+
txHash: z
|
|
43
|
+
.string()
|
|
44
|
+
.regex(TX_HASH_RE)
|
|
45
|
+
.describe('0x-prefixed 32-byte transaction hash of the anchor on base mainnet'),
|
|
46
|
+
},
|
|
47
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
48
|
+
}, async ({ payload, signature, txHash }) => {
|
|
49
|
+
try {
|
|
50
|
+
return ok(await verifyExecution({ payload, signature, txHash }, { rpcUrl: process.env.BASE_RPC_URL || DEFAULT_BASE_RPC_URL }));
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
return fail(err);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
server.registerTool('query_signers', {
|
|
57
|
+
description: 'list addresses that have anchored ExecutionLog records on Base, with anchor counts and first-seen block. free; reads ExecutionRecorded events directly over rpc.',
|
|
58
|
+
inputSchema: {
|
|
59
|
+
limit: z
|
|
60
|
+
.number()
|
|
61
|
+
.int()
|
|
62
|
+
.min(1)
|
|
63
|
+
.max(100)
|
|
64
|
+
.default(50)
|
|
65
|
+
.describe('max signers to return (1-100, default 50)'),
|
|
66
|
+
},
|
|
67
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
68
|
+
}, async ({ limit }) => {
|
|
69
|
+
try {
|
|
70
|
+
return ok(await querySigners({ limit, rpcUrl: process.env.BASE_RPC_URL || DEFAULT_BASE_RPC_URL }));
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
return fail(err);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
async function main() {
|
|
77
|
+
const transport = new StdioServerTransport();
|
|
78
|
+
await server.connect(transport);
|
|
79
|
+
console.error(`${SERVER_NAME} ${SERVER_VERSION} ready on stdio`);
|
|
80
|
+
}
|
|
81
|
+
main().catch((err) => {
|
|
82
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
83
|
+
process.exit(1);
|
|
84
|
+
});
|
|
85
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export declare const EXECUTION_RECORDED_EVENT: {
|
|
2
|
+
readonly name: "ExecutionRecorded";
|
|
3
|
+
readonly type: "event";
|
|
4
|
+
readonly inputs: readonly [{
|
|
5
|
+
readonly type: "address";
|
|
6
|
+
readonly name: "signer";
|
|
7
|
+
readonly indexed: true;
|
|
8
|
+
}, {
|
|
9
|
+
readonly type: "bytes32";
|
|
10
|
+
readonly name: "payloadHash";
|
|
11
|
+
readonly indexed: true;
|
|
12
|
+
}, {
|
|
13
|
+
readonly type: "uint256";
|
|
14
|
+
readonly name: "blockTimestamp";
|
|
15
|
+
}];
|
|
16
|
+
};
|
|
17
|
+
export interface ExecutionRecordedLog {
|
|
18
|
+
blockNumber: bigint;
|
|
19
|
+
args: {
|
|
20
|
+
signer: `0x${string}`;
|
|
21
|
+
payloadHash: `0x${string}`;
|
|
22
|
+
blockTimestamp: bigint;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export interface SignersRpcClient {
|
|
26
|
+
getBlockNumber(): Promise<bigint>;
|
|
27
|
+
getLogs(args: {
|
|
28
|
+
address: `0x${string}`;
|
|
29
|
+
event: typeof EXECUTION_RECORDED_EVENT;
|
|
30
|
+
fromBlock: bigint;
|
|
31
|
+
toBlock: bigint;
|
|
32
|
+
strict: true;
|
|
33
|
+
}): Promise<ExecutionRecordedLog[]>;
|
|
34
|
+
}
|
|
35
|
+
export interface SignerRow {
|
|
36
|
+
address: string;
|
|
37
|
+
anchor_count: number;
|
|
38
|
+
first_seen_block: number;
|
|
39
|
+
}
|
|
40
|
+
export interface QuerySignersResult {
|
|
41
|
+
signers: SignerRow[];
|
|
42
|
+
total_anchors: number;
|
|
43
|
+
}
|
|
44
|
+
export declare function makePublicRpcClient(rpcUrl: string): SignersRpcClient;
|
|
45
|
+
export declare function querySigners(opts?: {
|
|
46
|
+
limit?: number;
|
|
47
|
+
client?: SignersRpcClient;
|
|
48
|
+
rpcUrl?: string;
|
|
49
|
+
}): Promise<QuerySignersResult>;
|
package/dist/signers.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createPublicClient, http, parseAbiItem } from 'viem';
|
|
2
|
+
import { base } from 'viem/chains';
|
|
3
|
+
import { DEFAULT_BASE_RPC_URL, EXECUTION_LOG_ADDRESS, EXECUTION_LOG_DEPLOY_BLOCK, GET_LOGS_CHUNK_BLOCKS, } from './constants.js';
|
|
4
|
+
export const EXECUTION_RECORDED_EVENT = parseAbiItem('event ExecutionRecorded(address indexed signer, bytes32 indexed payloadHash, uint256 blockTimestamp)');
|
|
5
|
+
export function makePublicRpcClient(rpcUrl) {
|
|
6
|
+
const client = createPublicClient({ chain: base, transport: http(rpcUrl) });
|
|
7
|
+
return {
|
|
8
|
+
getBlockNumber: () => client.getBlockNumber(),
|
|
9
|
+
getLogs: (args) => client.getLogs(args),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export async function querySigners(opts = {}) {
|
|
13
|
+
const limit = Math.min(100, Math.max(1, Math.trunc(opts.limit ?? 50)));
|
|
14
|
+
const client = opts.client ?? makePublicRpcClient(opts.rpcUrl ?? DEFAULT_BASE_RPC_URL);
|
|
15
|
+
const tip = await client.getBlockNumber();
|
|
16
|
+
const bySigner = new Map();
|
|
17
|
+
let total = 0;
|
|
18
|
+
for (let from = EXECUTION_LOG_DEPLOY_BLOCK; from <= tip; from += GET_LOGS_CHUNK_BLOCKS) {
|
|
19
|
+
const chunkEnd = from + GET_LOGS_CHUNK_BLOCKS - 1n;
|
|
20
|
+
const logs = await client.getLogs({
|
|
21
|
+
address: EXECUTION_LOG_ADDRESS,
|
|
22
|
+
event: EXECUTION_RECORDED_EVENT,
|
|
23
|
+
fromBlock: from,
|
|
24
|
+
toBlock: chunkEnd < tip ? chunkEnd : tip,
|
|
25
|
+
strict: true,
|
|
26
|
+
});
|
|
27
|
+
for (const log of logs) {
|
|
28
|
+
total += 1;
|
|
29
|
+
const address = log.args.signer.toLowerCase();
|
|
30
|
+
const entry = bySigner.get(address);
|
|
31
|
+
if (!entry) {
|
|
32
|
+
bySigner.set(address, { count: 1, firstSeen: log.blockNumber });
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
entry.count += 1;
|
|
36
|
+
if (log.blockNumber < entry.firstSeen)
|
|
37
|
+
entry.firstSeen = log.blockNumber;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const signers = [...bySigner.entries()]
|
|
42
|
+
.map(([address, entry]) => ({
|
|
43
|
+
address,
|
|
44
|
+
anchor_count: entry.count,
|
|
45
|
+
first_seen_block: Number(entry.firstSeen),
|
|
46
|
+
}))
|
|
47
|
+
.sort((a, b) => b.anchor_count - a.anchor_count ||
|
|
48
|
+
(a.address < b.address ? -1 : a.address > b.address ? 1 : 0))
|
|
49
|
+
.slice(0, limit);
|
|
50
|
+
return { signers, total_anchors: total };
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=signers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signers.js","sourceRoot":"","sources":["../src/signers.ts"],"names":[],"mappings":"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"}
|
package/dist/verify.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type Hex, type Log } from 'viem';
|
|
2
|
+
export type VerifyToolResult = {
|
|
3
|
+
verified: true;
|
|
4
|
+
signer: string;
|
|
5
|
+
payloadHash: string;
|
|
6
|
+
signature: string;
|
|
7
|
+
block: number;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
txHash: string;
|
|
10
|
+
} | {
|
|
11
|
+
verified: false;
|
|
12
|
+
reason: string;
|
|
13
|
+
};
|
|
14
|
+
export interface VerifyInput {
|
|
15
|
+
payload: Record<string, unknown>;
|
|
16
|
+
signature: string;
|
|
17
|
+
txHash: string;
|
|
18
|
+
}
|
|
19
|
+
interface AnchorReceipt {
|
|
20
|
+
status: 'success' | 'reverted';
|
|
21
|
+
blockNumber: bigint;
|
|
22
|
+
logs: Log[];
|
|
23
|
+
}
|
|
24
|
+
export interface AnchorRpcClient {
|
|
25
|
+
getTransactionReceipt(args: {
|
|
26
|
+
hash: Hex;
|
|
27
|
+
}): Promise<AnchorReceipt>;
|
|
28
|
+
}
|
|
29
|
+
export declare function makeAnchorRpcClient(rpcUrl: string): AnchorRpcClient;
|
|
30
|
+
export declare function verifyExecution(input: VerifyInput, opts?: {
|
|
31
|
+
rpcUrl?: string;
|
|
32
|
+
client?: AnchorRpcClient;
|
|
33
|
+
}): Promise<VerifyToolResult>;
|
|
34
|
+
export {};
|
package/dist/verify.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { createPublicClient, http, isAddressEqual, parseEventLogs, recoverMessageAddress, TransactionReceiptNotFoundError, } from 'viem';
|
|
2
|
+
import { base } from 'viem/chains';
|
|
3
|
+
import { DEFAULT_BASE_RPC_URL, EXECUTION_LOG_ADDRESS, SIGNATURE_RE, TX_HASH_RE } from './constants.js';
|
|
4
|
+
import { EXECUTION_RECORDED_EVENT } from './signers.js';
|
|
5
|
+
import { payloadHash } from './canonical.js';
|
|
6
|
+
export function makeAnchorRpcClient(rpcUrl) {
|
|
7
|
+
const client = createPublicClient({ chain: base, transport: http(rpcUrl) });
|
|
8
|
+
return {
|
|
9
|
+
getTransactionReceipt: (args) => client.getTransactionReceipt(args),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
async function readAnchor(client, txHash) {
|
|
13
|
+
let receipt;
|
|
14
|
+
try {
|
|
15
|
+
receipt = await client.getTransactionReceipt({ hash: txHash });
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
if (err instanceof TransactionReceiptNotFoundError) {
|
|
19
|
+
return { ok: false, reason: 'no transaction found for txHash' };
|
|
20
|
+
}
|
|
21
|
+
throw err;
|
|
22
|
+
}
|
|
23
|
+
if (receipt.status === 'reverted') {
|
|
24
|
+
return { ok: false, reason: 'anchor transaction reverted' };
|
|
25
|
+
}
|
|
26
|
+
// parseEventLogs matches by event topic only, not by emitting address, so filter to
|
|
27
|
+
// our ExecutionLog instance: a tx emitting ExecutionRecorded from a different contract
|
|
28
|
+
// must not count. mirrors rsynth.fetch._verify.
|
|
29
|
+
const decoded = parseEventLogs({
|
|
30
|
+
abi: [EXECUTION_RECORDED_EVENT],
|
|
31
|
+
eventName: 'ExecutionRecorded',
|
|
32
|
+
logs: receipt.logs,
|
|
33
|
+
}).filter((log) => isAddressEqual(log.address, EXECUTION_LOG_ADDRESS));
|
|
34
|
+
if (decoded.length === 0) {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
reason: 'no ExecutionRecorded event from the ExecutionLog contract in this tx',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// v0.1 invariant: one record() call per tx -> one ExecutionRecorded per receipt.
|
|
41
|
+
const event = decoded[0];
|
|
42
|
+
return {
|
|
43
|
+
ok: true,
|
|
44
|
+
signer: event.args.signer,
|
|
45
|
+
payloadHash: event.args.payloadHash,
|
|
46
|
+
block: Number(receipt.blockNumber),
|
|
47
|
+
timestamp: Number(event.args.blockTimestamp),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// local verification (SCHEMA.md §2-5): recompute keccak256(canonical payload), recover the
|
|
51
|
+
// EIP-191 signer, read the on-chain anchor over public rpc, and decide locally. no remote
|
|
52
|
+
// verdict is relayed and no payment is made.
|
|
53
|
+
export async function verifyExecution(input, opts = {}) {
|
|
54
|
+
const { payload, signature, txHash } = input;
|
|
55
|
+
// boundary guards: reject malformed inputs before any rpc call.
|
|
56
|
+
if (!TX_HASH_RE.test(txHash)) {
|
|
57
|
+
throw new Error('txHash must be a 0x-prefixed 32-byte transaction hash');
|
|
58
|
+
}
|
|
59
|
+
if (!SIGNATURE_RE.test(signature)) {
|
|
60
|
+
throw new Error('signature must be a 0x-prefixed 65-byte hex string');
|
|
61
|
+
}
|
|
62
|
+
const client = opts.client ?? makeAnchorRpcClient(opts.rpcUrl ?? DEFAULT_BASE_RPC_URL);
|
|
63
|
+
const recompute = payloadHash(payload);
|
|
64
|
+
const recovered = await recoverMessageAddress({
|
|
65
|
+
message: { raw: recompute },
|
|
66
|
+
signature: signature,
|
|
67
|
+
});
|
|
68
|
+
const anchor = await readAnchor(client, txHash);
|
|
69
|
+
if (!anchor.ok) {
|
|
70
|
+
return { verified: false, reason: anchor.reason };
|
|
71
|
+
}
|
|
72
|
+
const hashMatch = recompute.toLowerCase() === anchor.payloadHash.toLowerCase();
|
|
73
|
+
const signerMatch = isAddressEqual(recovered, anchor.signer);
|
|
74
|
+
if (hashMatch && signerMatch) {
|
|
75
|
+
return {
|
|
76
|
+
verified: true,
|
|
77
|
+
signer: anchor.signer,
|
|
78
|
+
payloadHash: anchor.payloadHash,
|
|
79
|
+
signature,
|
|
80
|
+
block: anchor.block,
|
|
81
|
+
timestamp: anchor.timestamp,
|
|
82
|
+
txHash,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
verified: false,
|
|
87
|
+
reason: !hashMatch
|
|
88
|
+
? 'payload hash does not match the on-chain anchor'
|
|
89
|
+
: 'recovered signer does not match the on-chain anchor',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"payload": {
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"agent_id": 20617,
|
|
5
|
+
"robot_id": "sim-lerobot-pusht-ep1",
|
|
6
|
+
"episode_id": "ep_2026-06-19T15-04-10Z_pusht1",
|
|
7
|
+
"task": "push T to goal (lerobot/pusht reference replay)",
|
|
8
|
+
"started_at": "2026-06-19T15:04:10Z",
|
|
9
|
+
"ended_at": "2026-06-19T15:04:28Z",
|
|
10
|
+
"duration_seconds": 18.4,
|
|
11
|
+
"frames": 521,
|
|
12
|
+
"metrics": {
|
|
13
|
+
"rmse": 3.214,
|
|
14
|
+
"jerk": 1581043,
|
|
15
|
+
"end_variance": 0
|
|
16
|
+
},
|
|
17
|
+
"score": 0.88,
|
|
18
|
+
"outcome": "SUCCESS"
|
|
19
|
+
},
|
|
20
|
+
"payloadHash": "0x1d646db564305a51e5e617245a54bd6fc3bd457cb82488397ba38e1e1c559f26",
|
|
21
|
+
"signature": "0x30b36f9ec1bbb18a40247e52568a41c9c424adec7c5209cc7a4da9b47ca35be645b1243a430bf63845d1eb12858b321f2924b5264371a543358ba29f9a8d5c911b",
|
|
22
|
+
"signer": "0x82960f3322a7B1d2a2e756Efcbd4d1D56B613314",
|
|
23
|
+
"txHash": "0xee10ef6a112f05d9d3761aa055ab258f1afffd54f8c02212366e96a1da0915bb",
|
|
24
|
+
"blockNumber": 47568429,
|
|
25
|
+
"timestamp": 1781926205
|
|
26
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# verify the demo anchor
|
|
2
|
+
|
|
3
|
+
a complete `{payload, signature, txHash}` triple anchored on base mainnet · recompute it
|
|
4
|
+
yourself, no trust in this repo required.
|
|
5
|
+
|
|
6
|
+
the triple lives in [`demo-anchor.json`](./demo-anchor.json). `payloadHash` · `signature` ·
|
|
7
|
+
`signer` are deterministic (recompute them offline); `txHash` · `blockNumber` · `timestamp`
|
|
8
|
+
are filled when the anchor is minted (`pnpm mint:demo --broadcast`).
|
|
9
|
+
|
|
10
|
+
## verify it
|
|
11
|
+
|
|
12
|
+
call `verify_execution` with the `payload` · `signature` · `txHash` from `demo-anchor.json`:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
verify_execution {
|
|
16
|
+
"payload": { ...demo-anchor.json payload... },
|
|
17
|
+
"signature": "<demo-anchor.json signature>",
|
|
18
|
+
"txHash": "<demo-anchor.json txHash>"
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
expected (recomputed hash and recovered signer both match the anchor):
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"verified": true,
|
|
27
|
+
"signer": "<demo-anchor.json signer>",
|
|
28
|
+
"payloadHash": "<demo-anchor.json payloadHash>",
|
|
29
|
+
"signature": "<demo-anchor.json signature>",
|
|
30
|
+
"block": "<demo-anchor.json blockNumber>",
|
|
31
|
+
"timestamp": "<demo-anchor.json timestamp>",
|
|
32
|
+
"txHash": "<demo-anchor.json txHash>"
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## recompute it yourself
|
|
37
|
+
|
|
38
|
+
`verified` is not relayed from anywhere · the tool derives it locally:
|
|
39
|
+
|
|
40
|
+
- `keccak256(canonical_bytes(payload)) == payloadHash` (canonicalization: README §verify_execution).
|
|
41
|
+
- the eip-191 signer recovered from `signature` over that hash `== signer`.
|
|
42
|
+
- the `ExecutionLog` anchor read for `txHash` over public base rpc carries that same
|
|
43
|
+
`(signer, payloadHash)`.
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "r402-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "mcp server for the r402 execution-proof verifier on base mainnet",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"private": false,
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"r402-mcp": "dist/server.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"examples"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"prepublishOnly": "npm run build",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"mint:demo": "tsx scripts/mint-demo-anchor.ts"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
28
|
+
"viem": "^2.21.0",
|
|
29
|
+
"zod": "^4.0.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^22.0.0",
|
|
33
|
+
"tsx": "^4.22.4",
|
|
34
|
+
"typescript": "^5.6.0",
|
|
35
|
+
"vitest": "^2.1.0"
|
|
36
|
+
}
|
|
37
|
+
}
|