@parad0x_labs/mcp-server 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/README.md +88 -0
- package/dist/anchor.d.ts +29 -0
- package/dist/anchor.d.ts.map +1 -0
- package/dist/anchor.js +54 -0
- package/dist/anchor.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +985 -0
- package/dist/index.js.map +1 -0
- package/dist/resolve.d.ts +32 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +79 -0
- package/dist/resolve.js.map +1 -0
- package/dist/scope.d.ts +38 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +63 -0
- package/dist/scope.js.map +1 -0
- package/dist/wallet.d.ts +25 -0
- package/dist/wallet.d.ts.map +1 -0
- package/dist/wallet.js +32 -0
- package/dist/wallet.js.map +1 -0
- package/package.json +32 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,985 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { createHash, createHmac, randomBytes, createCipheriv, createSecretKey } from "crypto";
|
|
6
|
+
import { deflateSync } from "zlib";
|
|
7
|
+
import { Connection, PublicKey, Keypair, Transaction } from "@solana/web3.js";
|
|
8
|
+
import { WRITE_TOOLS, READ_TOOLS, assertNotSeized, canSubmitWrite, } from "./scope.js";
|
|
9
|
+
import { resolveNullName } from "./resolve.js";
|
|
10
|
+
import { buildAnchorIx, deriveBucketPda, bucketIdForUnix } from "./anchor.js";
|
|
11
|
+
import { generateWallet, resolveWalletPath } from "./wallet.js";
|
|
12
|
+
import { writeFileSync, existsSync, mkdirSync, chmodSync } from "fs";
|
|
13
|
+
import { dirname } from "path";
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Constants
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Live on Solana mainnet — verified against evidence/mainnet + evidence/zk.
|
|
18
|
+
// Full private-reputation stack live as of 2026-06-07 (reputation gate + commitment tree launched).
|
|
19
|
+
// WARNING: dark_x402_access_gate and dark_nullifier_record are SEIZED pre-incident IDs —
|
|
20
|
+
// deployer key F6Fr… stolen 2026-06-14; attacker holds upgrade authority. Do not call them.
|
|
21
|
+
const PROGRAMS = {
|
|
22
|
+
// SEIZED — pre-incident; attacker holds upgrade authority. Replaced by post-redeploy IDs (pending ceremony).
|
|
23
|
+
dark_x402_access_gate: "EepqzVBNuzCgD6XGiB19pDDhzFG3gUL4z1nabBYxpfjS",
|
|
24
|
+
// SEIZED — pre-incident; attacker holds upgrade authority. Replaced by post-redeploy IDs (pending ceremony).
|
|
25
|
+
dark_nullifier_record: "24tmjEd1DhPW2QuPV6BzkFFHrq2PtELoLqv5cuv2Xu65",
|
|
26
|
+
dark_reputation_gate: "9nN7UTTT5hgKnc2LZTqr3qaLLSt5PxWUrDbpUTGYHRxp",
|
|
27
|
+
receipt_commitment_tree: "8jC8QGiDJRRxhbPXMX5wJnGUq89xJZ2LsHMdbn2urCas",
|
|
28
|
+
receipt_anchor: "6HSRGivdYR5D7yTDy1TFMCM8h3LzXxRtKU1RA3RnCMRN",
|
|
29
|
+
dark_secp256r1_vault: "3hbbtjeSrTVYXq6eRwjeofDe2DCPh3n8cfN6kZcQfewi",
|
|
30
|
+
dark_secp256k1_auth: "AqwBbV13AoczhoELwP8oxT3nDqB6MsLWXauNzHkssZ9B",
|
|
31
|
+
dark_semaphore: "Ev7HEFhhKTXk6kS2Y6ssbUcK9C7E6yZ589jJNjUrQV5p",
|
|
32
|
+
null_token: "8EeDdvCRmFAzVD4takkBrNNwkeUTUQh4MscRK5Fzpump",
|
|
33
|
+
// demo/stub — NOT a real verifier (hardcoded proof bypass); see get_stack_status.
|
|
34
|
+
dark_bn254_gate: "GCptvBYF8S6eVYoh15B7WAESc54FUHCpN1Ui6aHeQYZd",
|
|
35
|
+
};
|
|
36
|
+
// Devnet-only addresses that differ from mainnet (the access gate has a separate devnet id;
|
|
37
|
+
// reputation gate, tree, and nullifier share the same id on both clusters).
|
|
38
|
+
const DEVNET_PROGRAMS = {
|
|
39
|
+
dark_x402_access_gate: "7LZzJnLSCCu2enc7mXz9FFCbomotME78xFG4eqkpo5U6",
|
|
40
|
+
};
|
|
41
|
+
const EXPLORER_BASE = "https://explorer.solana.com";
|
|
42
|
+
const DEFAULT_RPC = "https://solana-rpc.publicnode.com";
|
|
43
|
+
function explorerTx(sig) {
|
|
44
|
+
return `${EXPLORER_BASE}/tx/${sig}`;
|
|
45
|
+
}
|
|
46
|
+
function explorerAccount(addr) {
|
|
47
|
+
return `${EXPLORER_BASE}/address/${addr}`;
|
|
48
|
+
}
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Helpers
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
function sha256hex(data) {
|
|
53
|
+
return createHash("sha256")
|
|
54
|
+
.update(typeof data === "string" ? data : data)
|
|
55
|
+
.digest("hex");
|
|
56
|
+
}
|
|
57
|
+
function buildMerkleRoot(items) {
|
|
58
|
+
const leaves = items.map((item) => createHash("sha256").update(JSON.stringify(item)).digest());
|
|
59
|
+
if (leaves.length === 0)
|
|
60
|
+
return "0".repeat(64);
|
|
61
|
+
let layer = leaves;
|
|
62
|
+
while (layer.length > 1) {
|
|
63
|
+
const next = [];
|
|
64
|
+
for (let i = 0; i < layer.length; i += 2) {
|
|
65
|
+
const left = layer[i];
|
|
66
|
+
const right = (layer[i + 1] ?? layer[i]);
|
|
67
|
+
const combined = new Uint8Array(64);
|
|
68
|
+
combined.set(left, 0);
|
|
69
|
+
combined.set(right, 32);
|
|
70
|
+
next.push(createHash("sha256").update(combined).digest());
|
|
71
|
+
}
|
|
72
|
+
layer = next;
|
|
73
|
+
}
|
|
74
|
+
return Buffer.from(layer[0]).toString("hex");
|
|
75
|
+
}
|
|
76
|
+
function loadKeypair() {
|
|
77
|
+
const raw = process.env.SOLANA_KEYPAIR;
|
|
78
|
+
if (!raw)
|
|
79
|
+
return null;
|
|
80
|
+
try {
|
|
81
|
+
const bytes = JSON.parse(raw);
|
|
82
|
+
return Keypair.fromSecretKey(Uint8Array.from(bytes));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Zero-trust hardening — local-first, the LLM is untrusted
|
|
90
|
+
//
|
|
91
|
+
// This server runs on the agent's OWN machine (stdio). It never hosts anything,
|
|
92
|
+
// never custodies funds, and never lets key material reach the model context.
|
|
93
|
+
// 1. WRITES OFF BY DEFAULT: signing/submitting a tx needs the operator to opt
|
|
94
|
+
// in on this machine (PARAD0X_MCP_ALLOW_WRITE=1) AND a per-call confirm:true.
|
|
95
|
+
// 2. NO SECRETS TO THE LLM: every tool result is scrubbed of key material
|
|
96
|
+
// before it leaves this process; a short fingerprint is kept for correlation.
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
const ALLOW_WRITE = process.env.PARAD0X_MCP_ALLOW_WRITE === "1";
|
|
99
|
+
// Per-session consent registry — tracks which write operations the operator has
|
|
100
|
+
// explicitly consented to this session (beyond the ALLOW_WRITE env flag).
|
|
101
|
+
// Keys are tool names; value is the ISO timestamp when consent was granted.
|
|
102
|
+
// The scope/seized decision logic lives in ./scope.ts so it is unit-testable.
|
|
103
|
+
const sessionConsent = new Map();
|
|
104
|
+
const SECRET_FIELD = /(^|_)(key_hex|secret|secret_key|private_key|privatekey|mnemonic|seed|keypair|signing_key)($|_)/i;
|
|
105
|
+
function redactForLlm(value) {
|
|
106
|
+
if (Array.isArray(value))
|
|
107
|
+
return value.map(redactForLlm);
|
|
108
|
+
if (value && typeof value === "object") {
|
|
109
|
+
const out = {};
|
|
110
|
+
for (const [k, v] of Object.entries(value)) {
|
|
111
|
+
if (SECRET_FIELD.test(k) && typeof v === "string") {
|
|
112
|
+
out[k] = "[REDACTED — stays on this machine, never sent to the model]";
|
|
113
|
+
out[`${k}_fingerprint`] = sha256hex(v).slice(0, 16);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
out[k] = redactForLlm(v);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Tool implementations
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
async function x402GetQuote(endpointUrl, method = "GET") {
|
|
127
|
+
try {
|
|
128
|
+
const res = await fetch(endpointUrl, {
|
|
129
|
+
method,
|
|
130
|
+
headers: { Accept: "application/json" },
|
|
131
|
+
signal: AbortSignal.timeout(8000),
|
|
132
|
+
});
|
|
133
|
+
if (res.status === 402) {
|
|
134
|
+
const offerHeader = res.headers.get("x-dnp-offer") ?? res.headers.get("www-authenticate");
|
|
135
|
+
const body = await res.text().catch(() => "");
|
|
136
|
+
// Try to parse structured offer header
|
|
137
|
+
if (offerHeader) {
|
|
138
|
+
try {
|
|
139
|
+
const offer = JSON.parse(offerHeader);
|
|
140
|
+
return {
|
|
141
|
+
quote_id: offer.quote_id ?? sha256hex(endpointUrl + Date.now()).slice(0, 16),
|
|
142
|
+
price_atomic: offer.price_atomic ?? offer.amount ?? 0,
|
|
143
|
+
currency: offer.currency ?? "USDC",
|
|
144
|
+
expiry: offer.expiry ?? Date.now() + 60_000,
|
|
145
|
+
payment_address: offer.payment_address ?? offer.address ?? null,
|
|
146
|
+
network: offer.network ?? "solana-mainnet",
|
|
147
|
+
raw_offer: offer,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// header not JSON, fall through to body parse
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Try body
|
|
155
|
+
try {
|
|
156
|
+
const parsed = JSON.parse(body);
|
|
157
|
+
return {
|
|
158
|
+
quote_id: parsed.quote_id ?? sha256hex(endpointUrl + Date.now()).slice(0, 16),
|
|
159
|
+
price_atomic: parsed.price_atomic ?? parsed.amount ?? 0,
|
|
160
|
+
currency: parsed.currency ?? "USDC",
|
|
161
|
+
expiry: parsed.expiry ?? Date.now() + 60_000,
|
|
162
|
+
payment_address: parsed.payment_address ?? parsed.address ?? null,
|
|
163
|
+
network: parsed.network ?? "solana-mainnet",
|
|
164
|
+
raw_body: parsed,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// not parseable
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
quote_id: sha256hex(endpointUrl + Date.now()).slice(0, 16),
|
|
172
|
+
price_atomic: 0,
|
|
173
|
+
currency: "USDC",
|
|
174
|
+
expiry: Date.now() + 60_000,
|
|
175
|
+
payment_address: null,
|
|
176
|
+
network: "solana-mainnet",
|
|
177
|
+
note: "Endpoint returned 402 but offer format was not parseable. Raw header: " + (offerHeader ?? "(none)"),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// Not a real 402 endpoint — return mock format showing the structure
|
|
181
|
+
return {
|
|
182
|
+
quote_id: sha256hex(endpointUrl + Date.now()).slice(0, 16),
|
|
183
|
+
price_atomic: 100000, // 0.1 USDC in atomic units (6 decimals)
|
|
184
|
+
currency: "USDC",
|
|
185
|
+
expiry: Date.now() + 60_000,
|
|
186
|
+
payment_address: PROGRAMS.receipt_anchor,
|
|
187
|
+
network: "solana-mainnet",
|
|
188
|
+
note: `Endpoint returned HTTP ${res.status} (not 402). This is a mock quote showing the x402 format. A real x402-gated endpoint returns 402 with x-dnp-offer header.`,
|
|
189
|
+
mock: true,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
194
|
+
return { error: `Failed to reach endpoint: ${msg}`, mock: true };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async function anchorReceipt(receiptHashHex, rpcUrl = DEFAULT_RPC, confirm = false, consented = false) {
|
|
198
|
+
if (!/^[0-9a-fA-F]{64}$/.test(receiptHashHex)) {
|
|
199
|
+
return { error: "receipt_hash_hex must be exactly 64 hex characters (32 bytes)" };
|
|
200
|
+
}
|
|
201
|
+
const keypair = loadKeypair();
|
|
202
|
+
if (!keypair) {
|
|
203
|
+
// Dry-run mode — return mock response so agents can see the format
|
|
204
|
+
const mockSig = Buffer.from(randomBytes(64)).toString("base64url").slice(0, 88);
|
|
205
|
+
return {
|
|
206
|
+
solana_tx: mockSig,
|
|
207
|
+
explorer_url: explorerTx(mockSig),
|
|
208
|
+
slot: 0,
|
|
209
|
+
dry_run: true,
|
|
210
|
+
note: "SOLANA_KEYPAIR env var not set. Set it to a JSON array of 64 bytes to submit real transactions. This is a dry-run response showing the output format.",
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
// Zero-trust write-guard: a key is present, but do NOT submit unless the
|
|
214
|
+
// operator enabled writes on THIS machine AND the call is authorized — either
|
|
215
|
+
// by a per-call confirm:true or a prior session consent (grant_write_consent).
|
|
216
|
+
// Pin the hourly bucket client-side so the derived bucket PDA always matches
|
|
217
|
+
// the one the program writes (the no-bucket form risks an hour-boundary drift).
|
|
218
|
+
const bucketId = bucketIdForUnix(Math.floor(Date.now() / 1000));
|
|
219
|
+
const decision = canSubmitWrite({ allowWrite: ALLOW_WRITE, confirm, consented });
|
|
220
|
+
if (!decision.allowed) {
|
|
221
|
+
return {
|
|
222
|
+
preview: true,
|
|
223
|
+
would_submit: {
|
|
224
|
+
program: PROGRAMS.receipt_anchor,
|
|
225
|
+
instruction: "anchor v1 (version 0x01, pinned hourly bucket)",
|
|
226
|
+
receipt_hash_hex: receiptHashHex,
|
|
227
|
+
fee_payer: keypair.publicKey.toBase58(),
|
|
228
|
+
bucket_id: bucketId.toString(),
|
|
229
|
+
bucket_pda: deriveBucketPda(bucketId, PROGRAMS.receipt_anchor).toBase58(),
|
|
230
|
+
},
|
|
231
|
+
blocked_reason: decision.blockedReason,
|
|
232
|
+
note: "No transaction was sent. This is a preview of exactly what WOULD be submitted.",
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
const connection = new Connection(rpcUrl, "confirmed");
|
|
237
|
+
// receipt_anchor single-anchor, 42-byte pinned-bucket form (verified ABI):
|
|
238
|
+
// data [0x01][0x01][32B hash][u64 LE bucket_id]; keys payer + bucket PDA + system.
|
|
239
|
+
const ix = buildAnchorIx({
|
|
240
|
+
payer: keypair.publicKey.toBase58(),
|
|
241
|
+
receiptHashHex,
|
|
242
|
+
programId: PROGRAMS.receipt_anchor,
|
|
243
|
+
bucketId,
|
|
244
|
+
});
|
|
245
|
+
const tx = new Transaction().add(ix);
|
|
246
|
+
const { blockhash } = await connection.getLatestBlockhash();
|
|
247
|
+
tx.recentBlockhash = blockhash;
|
|
248
|
+
tx.feePayer = keypair.publicKey;
|
|
249
|
+
tx.sign(keypair);
|
|
250
|
+
const sig = await connection.sendRawTransaction(tx.serialize(), {
|
|
251
|
+
skipPreflight: false,
|
|
252
|
+
});
|
|
253
|
+
const conf = await connection.confirmTransaction(sig, "confirmed");
|
|
254
|
+
const slot = conf.context.slot;
|
|
255
|
+
return {
|
|
256
|
+
solana_tx: sig,
|
|
257
|
+
explorer_url: explorerTx(sig),
|
|
258
|
+
slot,
|
|
259
|
+
dry_run: false,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
264
|
+
return { error: `Solana transaction failed: ${msg}` };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async function lookupPassport(ethAddress, solanaWallet) {
|
|
268
|
+
if (!ethAddress && !solanaWallet) {
|
|
269
|
+
return { error: "Provide eth_address or solana_wallet (or both)" };
|
|
270
|
+
}
|
|
271
|
+
const programAddress = PROGRAMS.dark_secp256k1_auth;
|
|
272
|
+
const programId = new PublicKey(programAddress);
|
|
273
|
+
const rpcUrl = process.env.SOLANA_RPC_URL ?? DEFAULT_RPC;
|
|
274
|
+
// Derive the EthAgentRecord PDA
|
|
275
|
+
// Seeds: ["eth_agent", <eth_address_bytes_20>]
|
|
276
|
+
let pda = null;
|
|
277
|
+
let registered = false;
|
|
278
|
+
if (ethAddress) {
|
|
279
|
+
try {
|
|
280
|
+
const normalized = ethAddress.toLowerCase().replace("0x", "");
|
|
281
|
+
if (!/^[0-9a-f]{40}$/.test(normalized)) {
|
|
282
|
+
return { error: "eth_address must be a 40-hex-char Ethereum address (with or without 0x prefix)" };
|
|
283
|
+
}
|
|
284
|
+
const ethBytes = new Uint8Array(Buffer.from(normalized, "hex"));
|
|
285
|
+
const [derivedPda] = await PublicKey.findProgramAddress([Buffer.from("eth_agent"), ethBytes], programId);
|
|
286
|
+
pda = derivedPda.toBase58();
|
|
287
|
+
// Check if the account exists on-chain
|
|
288
|
+
const connection = new Connection(rpcUrl, "confirmed");
|
|
289
|
+
const info = await connection.getAccountInfo(derivedPda);
|
|
290
|
+
registered = info !== null && info.data.length > 0;
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
294
|
+
return { error: `PDA derivation failed: ${msg}` };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
else if (solanaWallet) {
|
|
298
|
+
// For solana-only lookup: check if the wallet has any record in the program
|
|
299
|
+
try {
|
|
300
|
+
const walletPk = new PublicKey(solanaWallet);
|
|
301
|
+
const [derivedPda] = await PublicKey.findProgramAddress([Buffer.from("sol_agent"), walletPk.toBytes()], programId);
|
|
302
|
+
pda = derivedPda.toBase58();
|
|
303
|
+
const connection = new Connection(rpcUrl, "confirmed");
|
|
304
|
+
const info = await connection.getAccountInfo(derivedPda);
|
|
305
|
+
registered = info !== null && info.data.length > 0;
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
309
|
+
return { error: `Solana wallet lookup failed: ${msg}` };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
registered,
|
|
314
|
+
pda: pda ?? undefined,
|
|
315
|
+
program: programAddress,
|
|
316
|
+
explorer_url: pda ? explorerAccount(pda) : explorerAccount(programAddress),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
async function checkNullifier(nullifier, rpcUrl = DEFAULT_RPC) {
|
|
320
|
+
assertNotSeized(PROGRAMS.dark_nullifier_record, "dark_nullifier_record");
|
|
321
|
+
// Accept a 64-char hex string OR a decimal BN254 field element.
|
|
322
|
+
const s = nullifier.trim();
|
|
323
|
+
let hex;
|
|
324
|
+
if (/^[0-9a-fA-F]{64}$/.test(s)) {
|
|
325
|
+
hex = s.toLowerCase();
|
|
326
|
+
}
|
|
327
|
+
else if (/^[0-9]+$/.test(s)) {
|
|
328
|
+
hex = BigInt(s).toString(16).padStart(64, "0");
|
|
329
|
+
if (hex.length !== 64)
|
|
330
|
+
return { error: "nullifier out of range — must fit in 32 bytes" };
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
return { error: "nullifier must be a 64-char hex string or a decimal field element" };
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
const nullifierBytes = Buffer.from(hex, "hex");
|
|
337
|
+
const program = new PublicKey(PROGRAMS.dark_nullifier_record);
|
|
338
|
+
// Single-use record PDA — matches the on-chain seed [b"null_record", nullifier_be_32].
|
|
339
|
+
const [recordPda] = PublicKey.findProgramAddressSync([Buffer.from("null_record"), nullifierBytes], program);
|
|
340
|
+
const connection = new Connection(rpcUrl, "confirmed");
|
|
341
|
+
const info = await connection.getAccountInfo(recordPda);
|
|
342
|
+
const spent = info !== null && info.data.length > 0;
|
|
343
|
+
return {
|
|
344
|
+
nullifier_hex: hex,
|
|
345
|
+
record_pda: recordPda.toBase58(),
|
|
346
|
+
spent,
|
|
347
|
+
program: PROGRAMS.dark_nullifier_record,
|
|
348
|
+
explorer_url: explorerAccount(recordPda.toBase58()),
|
|
349
|
+
note: spent
|
|
350
|
+
? "Nullifier already recorded on-chain — this proof has been spent (single-use exhausted)."
|
|
351
|
+
: "No record found — this nullifier has not been used yet.",
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
catch (err) {
|
|
355
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
356
|
+
return { error: `Nullifier lookup failed: ${msg}` };
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function buildOutcomeReceipt(params) {
|
|
360
|
+
const { receipt_id, outcome, metric_pnl, metric_accuracy, result_digest_hex, creator_note } = params;
|
|
361
|
+
// Build the outcome struct
|
|
362
|
+
const outcomeReceipt = {
|
|
363
|
+
schema: "parad0x/outcome-receipt/v1",
|
|
364
|
+
receipt_id,
|
|
365
|
+
outcome,
|
|
366
|
+
metrics: {
|
|
367
|
+
pnl: metric_pnl ?? null,
|
|
368
|
+
accuracy: metric_accuracy ?? null,
|
|
369
|
+
},
|
|
370
|
+
result_digest: result_digest_hex ?? null,
|
|
371
|
+
creator_note: creator_note ?? null,
|
|
372
|
+
created_at: Date.now(),
|
|
373
|
+
created_at_iso: new Date().toISOString(),
|
|
374
|
+
};
|
|
375
|
+
// Sign with an ephemeral key (no persistent signing key available in server context)
|
|
376
|
+
const ephemeralKeyHex = randomBytes(32).toString("hex");
|
|
377
|
+
const receiptStr = JSON.stringify(outcomeReceipt);
|
|
378
|
+
const sig = createHmac("sha256", ephemeralKeyHex).update(receiptStr).digest("hex");
|
|
379
|
+
const receiptHash = sha256hex(receiptStr);
|
|
380
|
+
return {
|
|
381
|
+
outcome_receipt: {
|
|
382
|
+
...outcomeReceipt,
|
|
383
|
+
signature: sig,
|
|
384
|
+
signing_note: "Signed with an ephemeral HMAC-SHA256 key generated at call time. For production use, provide a persistent Ed25519 keypair via SIGNING_KEYPAIR env var (not yet implemented).",
|
|
385
|
+
},
|
|
386
|
+
receipt_hash: receiptHash,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function compressReceipts(receipts) {
|
|
390
|
+
if (!Array.isArray(receipts) || receipts.length === 0) {
|
|
391
|
+
return { error: "receipts must be a non-empty array" };
|
|
392
|
+
}
|
|
393
|
+
const originalStr = JSON.stringify(receipts);
|
|
394
|
+
const originalBytes = Buffer.byteLength(originalStr, "utf8");
|
|
395
|
+
// Use zlib deflate as a proxy for Liquefy columnar compression
|
|
396
|
+
const compressed = deflateSync(originalStr, { level: 9 });
|
|
397
|
+
const compressedBytes = compressed.length;
|
|
398
|
+
const ratio = (originalBytes / compressedBytes).toFixed(2);
|
|
399
|
+
const merkleRootHex = buildMerkleRoot(receipts);
|
|
400
|
+
return {
|
|
401
|
+
compressed_base64: compressed.toString("base64"),
|
|
402
|
+
original_bytes: originalBytes,
|
|
403
|
+
compressed_bytes: compressedBytes,
|
|
404
|
+
ratio: `${ratio}x`,
|
|
405
|
+
merkle_root_hex: merkleRootHex,
|
|
406
|
+
receipt_count: receipts.length,
|
|
407
|
+
note: "Compressed with zlib deflate (level 9) as a format demonstration. Real Liquefy achieves ~83x via columnar layout + domain-aware encoding on typed receipt fields. Decompress with zlib inflate.",
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
async function privateCompute(params) {
|
|
411
|
+
const { plaintext_input, executor_endpoint, encryption_key_hex, anchor, rpc_url, consented } = params;
|
|
412
|
+
// Step 1: Generate or use provided 32-byte AES-256 key
|
|
413
|
+
let rawKeyBytes;
|
|
414
|
+
if (encryption_key_hex) {
|
|
415
|
+
if (!/^[0-9a-fA-F]{64}$/.test(encryption_key_hex)) {
|
|
416
|
+
return { error: "encryption_key_hex must be exactly 64 hex characters (32 bytes)" };
|
|
417
|
+
}
|
|
418
|
+
rawKeyBytes = new Uint8Array(Buffer.from(encryption_key_hex, "hex"));
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
rawKeyBytes = new Uint8Array(randomBytes(32));
|
|
422
|
+
}
|
|
423
|
+
const keyHex = Buffer.from(rawKeyBytes).toString("hex");
|
|
424
|
+
const secretKey = createSecretKey(rawKeyBytes);
|
|
425
|
+
// Step 2: Encrypt with AES-256-GCM (12-byte nonce prefix)
|
|
426
|
+
const nonceBytes = new Uint8Array(randomBytes(12));
|
|
427
|
+
const cipher = createCipheriv("aes-256-gcm", secretKey, nonceBytes);
|
|
428
|
+
const encPart1 = cipher.update(plaintext_input, "utf8");
|
|
429
|
+
const encPart2 = cipher.final();
|
|
430
|
+
const authTagBuf = cipher.getAuthTag();
|
|
431
|
+
const nonceCast = Buffer.from(nonceBytes);
|
|
432
|
+
// Layout: [12-byte nonce][16-byte auth tag][ciphertext]
|
|
433
|
+
const encryptedBlob = Buffer.concat([nonceCast, authTagBuf, encPart1, encPart2]);
|
|
434
|
+
const encryptedInputBase64 = encryptedBlob.toString("base64");
|
|
435
|
+
// Step 3: Compute input_hash = sha256(plaintext_input)
|
|
436
|
+
const inputHash = sha256hex(plaintext_input);
|
|
437
|
+
// Step 4: POST to executor_endpoint
|
|
438
|
+
let executorResponse = { status: "unreachable", note: "Executor endpoint could not be reached; local hashes recorded." };
|
|
439
|
+
try {
|
|
440
|
+
const res = await fetch(executor_endpoint, {
|
|
441
|
+
method: "POST",
|
|
442
|
+
headers: { "Content-Type": "application/json" },
|
|
443
|
+
body: JSON.stringify({ ciphertext: encryptedInputBase64, input_hash: inputHash }),
|
|
444
|
+
signal: AbortSignal.timeout(10000),
|
|
445
|
+
});
|
|
446
|
+
const text = await res.text().catch(() => "");
|
|
447
|
+
try {
|
|
448
|
+
executorResponse = JSON.parse(text);
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
executorResponse = { raw: text, http_status: res.status };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
catch (err) {
|
|
455
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
456
|
+
executorResponse = { status: "unreachable", error: msg, note: "Executor endpoint could not be reached; local hashes recorded." };
|
|
457
|
+
}
|
|
458
|
+
// Step 5: result_hash = sha256(JSON.stringify(executorResponse))
|
|
459
|
+
const resultHash = sha256hex(JSON.stringify(executorResponse));
|
|
460
|
+
// Step 6: Optionally anchor (input_hash + result_hash) on Solana
|
|
461
|
+
let commitmentTx;
|
|
462
|
+
let explorerUrl;
|
|
463
|
+
if (anchor) {
|
|
464
|
+
// Commitment = sha256(input_hash_bytes + result_hash_bytes)
|
|
465
|
+
const ihBuf = Buffer.from(inputHash, "hex");
|
|
466
|
+
const rhBuf = Buffer.from(resultHash, "hex");
|
|
467
|
+
const combined = Buffer.concat([ihBuf, rhBuf]);
|
|
468
|
+
const commitmentHex = createHash("sha256").update(combined).digest("hex");
|
|
469
|
+
const anchorResult = await anchorReceipt(commitmentHex, rpc_url ?? process.env.SOLANA_RPC_URL ?? DEFAULT_RPC, false, consented === true);
|
|
470
|
+
if (anchorResult.solana_tx) {
|
|
471
|
+
commitmentTx = anchorResult.solana_tx;
|
|
472
|
+
explorerUrl = anchorResult.explorer_url;
|
|
473
|
+
}
|
|
474
|
+
else if (anchorResult.error) {
|
|
475
|
+
commitmentTx = `anchor_failed: ${anchorResult.error}`;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
// Step 7: Return all fields
|
|
479
|
+
const output = {
|
|
480
|
+
encrypted_input_base64: encryptedInputBase64,
|
|
481
|
+
input_hash: inputHash,
|
|
482
|
+
executor_response: executorResponse,
|
|
483
|
+
result_hash: resultHash,
|
|
484
|
+
key_hex: keyHex,
|
|
485
|
+
protocol_note: "executor received ciphertext only — plaintext never left client",
|
|
486
|
+
};
|
|
487
|
+
if (commitmentTx !== undefined)
|
|
488
|
+
output.commitment_tx = commitmentTx;
|
|
489
|
+
if (explorerUrl !== undefined)
|
|
490
|
+
output.explorer_url = explorerUrl;
|
|
491
|
+
return output;
|
|
492
|
+
}
|
|
493
|
+
function getStackStatus() {
|
|
494
|
+
assertNotSeized(PROGRAMS.dark_x402_access_gate, "dark_x402_access_gate");
|
|
495
|
+
assertNotSeized(PROGRAMS.dark_nullifier_record, "dark_nullifier_record");
|
|
496
|
+
return {
|
|
497
|
+
programs: [
|
|
498
|
+
{
|
|
499
|
+
name: "dark_x402_access_gate",
|
|
500
|
+
address: PROGRAMS.dark_x402_access_gate,
|
|
501
|
+
status: "live (mainnet) — VK single-party until the trusted-setup ceremony finalizes",
|
|
502
|
+
explorer_url: explorerAccount(PROGRAMS.dark_x402_access_gate),
|
|
503
|
+
description: "Groth16 BN254 access gate — prove funded + authorized WITHOUT revealing wallet or balance. On-chain verify via alt_bn128_pairing (~93k CU, ~$0.0007).",
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
name: "dark_nullifier_record",
|
|
507
|
+
address: PROGRAMS.dark_nullifier_record,
|
|
508
|
+
status: "live",
|
|
509
|
+
explorer_url: explorerAccount(PROGRAMS.dark_nullifier_record),
|
|
510
|
+
description: "Single-use / anti-replay — records a nullifier PDA so each proof is spendable exactly once.",
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
name: "receipt_anchor",
|
|
514
|
+
address: PROGRAMS.receipt_anchor,
|
|
515
|
+
status: "live",
|
|
516
|
+
explorer_url: explorerAccount(PROGRAMS.receipt_anchor),
|
|
517
|
+
description: "Anchors 32-byte receipt hashes permanently on Solana mainnet",
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
name: "dark_secp256r1_vault",
|
|
521
|
+
address: PROGRAMS.dark_secp256r1_vault,
|
|
522
|
+
status: "live",
|
|
523
|
+
explorer_url: explorerAccount(PROGRAMS.dark_secp256r1_vault),
|
|
524
|
+
description: "WebAuthn / P-256 vault — stores secp256r1 public keys on-chain",
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
name: "dark_secp256k1_auth",
|
|
528
|
+
address: PROGRAMS.dark_secp256k1_auth,
|
|
529
|
+
status: "live",
|
|
530
|
+
explorer_url: explorerAccount(PROGRAMS.dark_secp256k1_auth),
|
|
531
|
+
description: "ETH address binding — links MetaMask / secp256k1 identities to Solana wallets",
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
name: "dark_bn254_gate",
|
|
535
|
+
address: PROGRAMS.dark_bn254_gate,
|
|
536
|
+
status: "stub — do not use",
|
|
537
|
+
explorer_url: explorerAccount(PROGRAMS.dark_bn254_gate),
|
|
538
|
+
description: "DEPRECATED demo gate with a hardcoded proof bypass — NOT a real verifier. Superseded by dark_x402_access_gate (real Groth16 BN254). Listed for transparency only.",
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
name: "dark_semaphore",
|
|
542
|
+
address: PROGRAMS.dark_semaphore,
|
|
543
|
+
status: "live",
|
|
544
|
+
explorer_url: explorerAccount(PROGRAMS.dark_semaphore),
|
|
545
|
+
description: "Semaphore-style anonymous group membership proofs",
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
name: "null_token",
|
|
549
|
+
address: PROGRAMS.null_token,
|
|
550
|
+
status: "live",
|
|
551
|
+
explorer_url: explorerAccount(PROGRAMS.null_token),
|
|
552
|
+
description: "NULL SPL token — native currency of the Parad0x Labs protocol economy",
|
|
553
|
+
},
|
|
554
|
+
],
|
|
555
|
+
private_reputation_stack: {
|
|
556
|
+
note: "Private track-record proof — LIVE on mainnet as of 2026-06-07. Single-party VK until the trusted-setup ceremony finalizes (same status as the access gate).",
|
|
557
|
+
dark_reputation_gate: PROGRAMS.dark_reputation_gate,
|
|
558
|
+
receipt_commitment_tree: PROGRAMS.receipt_commitment_tree,
|
|
559
|
+
},
|
|
560
|
+
packages: [
|
|
561
|
+
"@parad0x_labs/mcp-server",
|
|
562
|
+
"@parad0x_labs/null-miner-sdk",
|
|
563
|
+
"@parad0x_labs/liquefy-receipts",
|
|
564
|
+
"@parad0x_labs/outcome-receipts",
|
|
565
|
+
"@parad0x_labs/pay-to-receive",
|
|
566
|
+
"@parad0x_labs/receipt-dag",
|
|
567
|
+
"@parad0x_labs/session-channels",
|
|
568
|
+
"@parad0x_labs/task-marketplace-api",
|
|
569
|
+
"@parad0x_labs/nulllive-sdk",
|
|
570
|
+
],
|
|
571
|
+
github: "https://github.com/parad0x-labs/dna-x402",
|
|
572
|
+
network: "solana-mainnet",
|
|
573
|
+
status_timestamp: new Date().toISOString(),
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
// ---------------------------------------------------------------------------
|
|
577
|
+
// MCP server setup
|
|
578
|
+
// ---------------------------------------------------------------------------
|
|
579
|
+
const server = new Server({ name: "parad0x-mcp", version: "0.1.0" }, {
|
|
580
|
+
capabilities: {
|
|
581
|
+
tools: {},
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
585
|
+
return {
|
|
586
|
+
tools: [
|
|
587
|
+
{
|
|
588
|
+
name: "x402_get_quote",
|
|
589
|
+
description: "Get a payment quote for an x402-gated API endpoint",
|
|
590
|
+
inputSchema: {
|
|
591
|
+
type: "object",
|
|
592
|
+
properties: {
|
|
593
|
+
endpoint_url: {
|
|
594
|
+
type: "string",
|
|
595
|
+
description: "The URL of the x402-gated endpoint to quote",
|
|
596
|
+
},
|
|
597
|
+
method: {
|
|
598
|
+
type: "string",
|
|
599
|
+
description: "HTTP method (default: GET)",
|
|
600
|
+
enum: ["GET", "POST", "PUT", "DELETE", "PATCH"],
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
required: ["endpoint_url"],
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
name: "anchor_receipt",
|
|
608
|
+
description: "Anchor a 32-byte receipt hash permanently on Solana mainnet via the receipt_anchor program. Read-only by default: returns a PREVIEW unless the operator enabled writes (PARAD0X_MCP_ALLOW_WRITE=1) AND you pass confirm:true.",
|
|
609
|
+
inputSchema: {
|
|
610
|
+
type: "object",
|
|
611
|
+
properties: {
|
|
612
|
+
receipt_hash_hex: {
|
|
613
|
+
type: "string",
|
|
614
|
+
description: "64-character hex string representing the 32-byte SHA-256 receipt hash",
|
|
615
|
+
},
|
|
616
|
+
rpc_url: {
|
|
617
|
+
type: "string",
|
|
618
|
+
description: "Solana RPC URL (default: publicnode public mainnet RPC)",
|
|
619
|
+
},
|
|
620
|
+
confirm: {
|
|
621
|
+
type: "boolean",
|
|
622
|
+
description: "Must be true to actually submit. Without it the tool returns a preview only (no transaction sent).",
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
required: ["receipt_hash_hex"],
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
name: "lookup_passport",
|
|
630
|
+
description: "Look up a Dark Passport — check if an ETH address or Solana wallet has a verified identity binding on Solana mainnet",
|
|
631
|
+
inputSchema: {
|
|
632
|
+
type: "object",
|
|
633
|
+
properties: {
|
|
634
|
+
eth_address: {
|
|
635
|
+
type: "string",
|
|
636
|
+
description: "Ethereum address (hex, with or without 0x prefix)",
|
|
637
|
+
},
|
|
638
|
+
solana_wallet: {
|
|
639
|
+
type: "string",
|
|
640
|
+
description: "Solana wallet address (base58)",
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
name: "build_outcome_receipt",
|
|
647
|
+
description: "Build a creator-signed outcome receipt — attach PnL, accuracy, or delivery result to a previous receipt",
|
|
648
|
+
inputSchema: {
|
|
649
|
+
type: "object",
|
|
650
|
+
properties: {
|
|
651
|
+
receipt_id: {
|
|
652
|
+
type: "string",
|
|
653
|
+
description: "ID of the receipt this outcome is attached to",
|
|
654
|
+
},
|
|
655
|
+
outcome: {
|
|
656
|
+
type: "string",
|
|
657
|
+
enum: ["positive", "negative", "neutral"],
|
|
658
|
+
description: "Outcome classification",
|
|
659
|
+
},
|
|
660
|
+
metric_pnl: {
|
|
661
|
+
type: "number",
|
|
662
|
+
description: "Profit/loss metric (optional)",
|
|
663
|
+
},
|
|
664
|
+
metric_accuracy: {
|
|
665
|
+
type: "number",
|
|
666
|
+
description: "Accuracy metric 0.0–1.0 (optional)",
|
|
667
|
+
},
|
|
668
|
+
result_digest_hex: {
|
|
669
|
+
type: "string",
|
|
670
|
+
description: "Hex-encoded SHA-256 of the result payload (optional)",
|
|
671
|
+
},
|
|
672
|
+
creator_note: {
|
|
673
|
+
type: "string",
|
|
674
|
+
description: "Human-readable note from the creator (optional)",
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
required: ["receipt_id", "outcome"],
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
name: "compress_receipts",
|
|
682
|
+
description: "Compress a batch of receipts using Liquefy columnar compression (83x typical ratio)",
|
|
683
|
+
inputSchema: {
|
|
684
|
+
type: "object",
|
|
685
|
+
properties: {
|
|
686
|
+
receipts: {
|
|
687
|
+
type: "array",
|
|
688
|
+
items: { type: "object" },
|
|
689
|
+
description: "Array of receipt objects to compress",
|
|
690
|
+
},
|
|
691
|
+
},
|
|
692
|
+
required: ["receipts"],
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
name: "check_nullifier",
|
|
697
|
+
description: "Check whether a privacy-proof nullifier has already been spent on Solana (single-use enforcement). Read-only: derives the dark_nullifier_record PDA and checks if it exists on-chain. No signing, no funds.",
|
|
698
|
+
inputSchema: {
|
|
699
|
+
type: "object",
|
|
700
|
+
properties: {
|
|
701
|
+
nullifier: {
|
|
702
|
+
type: "string",
|
|
703
|
+
description: "The nullifier as a 64-char hex string OR a decimal BN254 field element (e.g. the reputation_nullifier public input).",
|
|
704
|
+
},
|
|
705
|
+
rpc_url: {
|
|
706
|
+
type: "string",
|
|
707
|
+
description: "Solana RPC URL (default: publicnode mainnet). Use a devnet RPC to check the devnet stack.",
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
required: ["nullifier"],
|
|
711
|
+
},
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
name: "get_stack_status",
|
|
715
|
+
description: "Get the current status of all Parad0x Labs mainnet programs",
|
|
716
|
+
inputSchema: {
|
|
717
|
+
type: "object",
|
|
718
|
+
properties: {},
|
|
719
|
+
},
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
name: "private_compute",
|
|
723
|
+
description: "Run a computation via an executor endpoint without exposing plaintext inputs. Agent encrypts locally, sends ciphertext, executor returns encrypted result + result hash. Commit (input_hash, result_hash) on Solana. Executor never sees plaintext.",
|
|
724
|
+
inputSchema: {
|
|
725
|
+
type: "object",
|
|
726
|
+
properties: {
|
|
727
|
+
plaintext_input: {
|
|
728
|
+
type: "string",
|
|
729
|
+
description: "The sensitive input (JSON string or text) to encrypt before sending",
|
|
730
|
+
},
|
|
731
|
+
executor_endpoint: {
|
|
732
|
+
type: "string",
|
|
733
|
+
description: "URL of the executor API that receives the ciphertext",
|
|
734
|
+
},
|
|
735
|
+
encryption_key_hex: {
|
|
736
|
+
type: "string",
|
|
737
|
+
description: "Optional 32-byte AES-256 key as 64 hex characters. Generated if not provided.",
|
|
738
|
+
},
|
|
739
|
+
anchor: {
|
|
740
|
+
type: "boolean",
|
|
741
|
+
description: "If true, commit (input_hash, result_hash) to Solana via receipt_anchor",
|
|
742
|
+
},
|
|
743
|
+
rpc_url: {
|
|
744
|
+
type: "string",
|
|
745
|
+
description: "Solana RPC URL (default: publicnode public mainnet RPC)",
|
|
746
|
+
},
|
|
747
|
+
},
|
|
748
|
+
required: ["plaintext_input", "executor_endpoint"],
|
|
749
|
+
},
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
name: "create_wallet",
|
|
753
|
+
description: "Generate a NEW non-custodial Solana wallet on THIS machine for the agent/owner. Writes the secret key to a local file (default ~/.config/solana/web0-agent.json) and returns ONLY the public key + path — the secret is never shown to the model. Refuses to overwrite an existing file. Preview by default; pass confirm:true to actually generate + write.",
|
|
754
|
+
inputSchema: {
|
|
755
|
+
type: "object",
|
|
756
|
+
properties: {
|
|
757
|
+
label: {
|
|
758
|
+
type: "string",
|
|
759
|
+
description: "Filename label for the default path (~/.config/solana/<label>.json). Default: web0-agent.",
|
|
760
|
+
},
|
|
761
|
+
path: {
|
|
762
|
+
type: "string",
|
|
763
|
+
description: "Explicit keypair file path (overrides label). A leading ~ is expanded to your home dir.",
|
|
764
|
+
},
|
|
765
|
+
confirm: {
|
|
766
|
+
type: "boolean",
|
|
767
|
+
description: "Must be true to generate + write the key file. Without it, returns a preview only.",
|
|
768
|
+
},
|
|
769
|
+
},
|
|
770
|
+
},
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
name: "resolve_null",
|
|
774
|
+
description: "Resolve a .null name on Solana mainnet → its owner, published x402 endpoint (pay-by-name), stealth meta-address, and Arweave content. Read-only: derives the NullDomain PDA on the live registrar and reads it. Returns payable_by_name=true when an x402 endpoint is set.",
|
|
775
|
+
inputSchema: {
|
|
776
|
+
type: "object",
|
|
777
|
+
properties: {
|
|
778
|
+
name: {
|
|
779
|
+
type: "string",
|
|
780
|
+
description: "The .null name to resolve (e.g. \"myagent.null\" or \"myagent\").",
|
|
781
|
+
},
|
|
782
|
+
rpc_url: {
|
|
783
|
+
type: "string",
|
|
784
|
+
description: "Solana mainnet RPC URL (default: publicnode mainnet). The registrar is on mainnet.",
|
|
785
|
+
},
|
|
786
|
+
},
|
|
787
|
+
required: ["name"],
|
|
788
|
+
},
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
name: "get_scope_status",
|
|
792
|
+
description: "Show the current permission scope for this MCP session — which tools require elevated consent, which are currently consented, and whether write mode is enabled by the operator.",
|
|
793
|
+
inputSchema: { type: "object", properties: {} },
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
name: "grant_write_consent",
|
|
797
|
+
description: "Explicitly consent to a write operation for this session. Must be called before anchor_receipt or private_compute if confirm:true alone is not sufficient. The operator must have set PARAD0X_MCP_ALLOW_WRITE=1.",
|
|
798
|
+
inputSchema: {
|
|
799
|
+
type: "object",
|
|
800
|
+
properties: {
|
|
801
|
+
tool_name: {
|
|
802
|
+
type: "string",
|
|
803
|
+
enum: ["anchor_receipt", "private_compute"],
|
|
804
|
+
description: "Which write tool to grant session consent for",
|
|
805
|
+
},
|
|
806
|
+
},
|
|
807
|
+
required: ["tool_name"],
|
|
808
|
+
},
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
name: "revoke_write_consent",
|
|
812
|
+
description: "Revoke session consent for a write tool — it will require re-confirmation.",
|
|
813
|
+
inputSchema: {
|
|
814
|
+
type: "object",
|
|
815
|
+
properties: {
|
|
816
|
+
tool_name: {
|
|
817
|
+
type: "string",
|
|
818
|
+
enum: ["anchor_receipt", "private_compute"],
|
|
819
|
+
description: "Which write tool to revoke consent for",
|
|
820
|
+
},
|
|
821
|
+
},
|
|
822
|
+
required: ["tool_name"],
|
|
823
|
+
},
|
|
824
|
+
},
|
|
825
|
+
],
|
|
826
|
+
};
|
|
827
|
+
});
|
|
828
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
829
|
+
const { name, arguments: args } = request.params;
|
|
830
|
+
try {
|
|
831
|
+
let result;
|
|
832
|
+
switch (name) {
|
|
833
|
+
case "x402_get_quote": {
|
|
834
|
+
const { endpoint_url, method } = args;
|
|
835
|
+
result = await x402GetQuote(endpoint_url, method);
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
838
|
+
case "anchor_receipt": {
|
|
839
|
+
const { receipt_hash_hex, rpc_url, confirm } = args;
|
|
840
|
+
result = await anchorReceipt(receipt_hash_hex, rpc_url ?? process.env.SOLANA_RPC_URL ?? DEFAULT_RPC, confirm === true, sessionConsent.has("anchor_receipt"));
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
case "lookup_passport": {
|
|
844
|
+
const { eth_address, solana_wallet } = args;
|
|
845
|
+
result = await lookupPassport(eth_address, solana_wallet);
|
|
846
|
+
break;
|
|
847
|
+
}
|
|
848
|
+
case "build_outcome_receipt": {
|
|
849
|
+
result = buildOutcomeReceipt(args);
|
|
850
|
+
break;
|
|
851
|
+
}
|
|
852
|
+
case "compress_receipts": {
|
|
853
|
+
const { receipts } = args;
|
|
854
|
+
result = compressReceipts(receipts);
|
|
855
|
+
break;
|
|
856
|
+
}
|
|
857
|
+
case "check_nullifier": {
|
|
858
|
+
const { nullifier, rpc_url } = args;
|
|
859
|
+
result = await checkNullifier(nullifier, rpc_url ?? process.env.SOLANA_RPC_URL ?? DEFAULT_RPC);
|
|
860
|
+
break;
|
|
861
|
+
}
|
|
862
|
+
case "get_stack_status": {
|
|
863
|
+
result = getStackStatus();
|
|
864
|
+
break;
|
|
865
|
+
}
|
|
866
|
+
case "private_compute": {
|
|
867
|
+
result = await privateCompute({
|
|
868
|
+
...args,
|
|
869
|
+
consented: sessionConsent.has("private_compute"),
|
|
870
|
+
});
|
|
871
|
+
break;
|
|
872
|
+
}
|
|
873
|
+
case "create_wallet": {
|
|
874
|
+
const { label, path: wpath, confirm: wconfirm } = args;
|
|
875
|
+
const target = resolveWalletPath({ path: wpath, label });
|
|
876
|
+
if (existsSync(target)) {
|
|
877
|
+
result = { error: `Refusing to overwrite an existing keypair at ${target}. Choose a different label or path.` };
|
|
878
|
+
break;
|
|
879
|
+
}
|
|
880
|
+
if (wconfirm !== true) {
|
|
881
|
+
result = {
|
|
882
|
+
preview: true,
|
|
883
|
+
would_create_at: target,
|
|
884
|
+
note: "No wallet created. Pass confirm:true to generate + write a new keypair here. The secret key is written only to this file on your machine and is NEVER shown to the model.",
|
|
885
|
+
};
|
|
886
|
+
break;
|
|
887
|
+
}
|
|
888
|
+
try {
|
|
889
|
+
const w = generateWallet();
|
|
890
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
891
|
+
writeFileSync(target, JSON.stringify(w.secretKey), { mode: 0o600 });
|
|
892
|
+
try {
|
|
893
|
+
chmodSync(target, 0o600);
|
|
894
|
+
}
|
|
895
|
+
catch {
|
|
896
|
+
/* best-effort on platforms without POSIX perms */
|
|
897
|
+
}
|
|
898
|
+
result = {
|
|
899
|
+
created: true,
|
|
900
|
+
public_key: w.publicKey,
|
|
901
|
+
keypair_path: target,
|
|
902
|
+
funded: false,
|
|
903
|
+
next_steps: [
|
|
904
|
+
"Fund this address with a little SOL (for transaction fees) and USDC (to spend).",
|
|
905
|
+
"Point your signer at this file (e.g. SOLANA_KEYPAIR or your wallet config) to pay / register / anchor.",
|
|
906
|
+
"BACK IT UP — this file is the only copy of the key; losing it loses the funds.",
|
|
907
|
+
],
|
|
908
|
+
security: "The secret key was written only to the file above on this machine. It was NOT returned to the model.",
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
catch (err) {
|
|
912
|
+
result = { error: `Failed to create wallet: ${err instanceof Error ? err.message : String(err)}` };
|
|
913
|
+
}
|
|
914
|
+
break;
|
|
915
|
+
}
|
|
916
|
+
case "resolve_null": {
|
|
917
|
+
const { name: nullName, rpc_url } = args;
|
|
918
|
+
const r = await resolveNullName(nullName, rpc_url);
|
|
919
|
+
result = {
|
|
920
|
+
...r,
|
|
921
|
+
payable_by_name: !!r.x402_endpoint,
|
|
922
|
+
explorer_url: explorerAccount(r.pda),
|
|
923
|
+
note: r.found
|
|
924
|
+
? r.x402_endpoint
|
|
925
|
+
? "Resolved — payable by name via the published x402 endpoint."
|
|
926
|
+
: "Registered, but no x402 endpoint published yet (the owner must set one via UPDATE_ENDPOINT)."
|
|
927
|
+
: "Not registered on mainnet.",
|
|
928
|
+
};
|
|
929
|
+
break;
|
|
930
|
+
}
|
|
931
|
+
case "get_scope_status": {
|
|
932
|
+
result = {
|
|
933
|
+
write_mode_enabled: ALLOW_WRITE,
|
|
934
|
+
read_tools: [...READ_TOOLS].sort(),
|
|
935
|
+
write_tools: [...WRITE_TOOLS].map((t) => ({
|
|
936
|
+
tool: t,
|
|
937
|
+
consented: sessionConsent.has(t),
|
|
938
|
+
consented_at: sessionConsent.get(t) ?? null,
|
|
939
|
+
})),
|
|
940
|
+
note: ALLOW_WRITE
|
|
941
|
+
? "Write mode active. Call grant_write_consent to pre-approve a specific tool, or pass confirm:true per-call."
|
|
942
|
+
: "Write mode disabled (PARAD0X_MCP_ALLOW_WRITE not set). Read-only tools available.",
|
|
943
|
+
};
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
946
|
+
case "grant_write_consent": {
|
|
947
|
+
const { tool_name } = args;
|
|
948
|
+
if (!WRITE_TOOLS.has(tool_name)) {
|
|
949
|
+
result = { error: `${tool_name} is not a write tool` };
|
|
950
|
+
}
|
|
951
|
+
else if (!ALLOW_WRITE) {
|
|
952
|
+
result = { error: "Cannot grant consent — PARAD0X_MCP_ALLOW_WRITE=1 required on this machine" };
|
|
953
|
+
}
|
|
954
|
+
else {
|
|
955
|
+
sessionConsent.set(tool_name, new Date().toISOString());
|
|
956
|
+
result = { granted: true, tool: tool_name, consented_at: sessionConsent.get(tool_name) };
|
|
957
|
+
}
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
960
|
+
case "revoke_write_consent": {
|
|
961
|
+
const { tool_name: revokeTarget } = args;
|
|
962
|
+
const had = sessionConsent.delete(revokeTarget);
|
|
963
|
+
result = { revoked: had, tool: revokeTarget };
|
|
964
|
+
break;
|
|
965
|
+
}
|
|
966
|
+
default:
|
|
967
|
+
result = { error: `Unknown tool: ${name}` };
|
|
968
|
+
}
|
|
969
|
+
return {
|
|
970
|
+
content: [{ type: "text", text: JSON.stringify(redactForLlm(result), null, 2) }],
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
catch (err) {
|
|
974
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
975
|
+
return {
|
|
976
|
+
content: [{ type: "text", text: JSON.stringify({ error: msg }) }],
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
// ---------------------------------------------------------------------------
|
|
981
|
+
// Start
|
|
982
|
+
// ---------------------------------------------------------------------------
|
|
983
|
+
const transport = new StdioServerTransport();
|
|
984
|
+
await server.connect(transport);
|
|
985
|
+
//# sourceMappingURL=index.js.map
|