@iamnotdou/ccp 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/dist/mcp.js ADDED
@@ -0,0 +1,648 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CCP MCP Server — Model Context Protocol server for the Containment Certificate Protocol
4
+ *
5
+ * Exposes all CCP protocol operations as MCP tools so AI agents (e.g. OpenClaw)
6
+ * can interact with the protocol natively without shelling out to a CLI.
7
+ *
8
+ * Usage:
9
+ * npx ccp-cli mcp # stdio transport (for Claude, OpenClaw, etc.)
10
+ * Add to claude_desktop_config.json:
11
+ * {
12
+ * "mcpServers": {
13
+ * "ccp": { "command": "npx", "args": ["ccp-cli", "mcp"] }
14
+ * }
15
+ * }
16
+ */
17
+ import "dotenv/config";
18
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
19
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
20
+ import { z } from "zod";
21
+ import { formatUnits, formatEther, parseUnits, keccak256, encodePacked, } from "viem";
22
+ import { privateKeyToAccount } from "viem/accounts";
23
+ import { publicClient, getWalletClient } from "./client.js";
24
+ import { addresses, keys, hederaConfig } from "./config.js";
25
+ import { CCPRegistryABI, ReserveVaultABI, SpendingLimitABI, AuditorStakingABI, ChallengeManagerABI, } from "./contracts/index.js";
26
+ import { attestCertificate } from "./auditor/attest.js";
27
+ import { ledgerSignCertificate, executeWithLedgerCosign, } from "./ledger/cosigner.js";
28
+ import { publishCertificatePublished, publishAgentTransaction, publishTransactionBlocked, } from "./hcs/publisher.js";
29
+ import { getTopicMessages } from "./hedera/mirrorNode.js";
30
+ // ─── Helpers ───
31
+ const USDC_DECIMALS = 6;
32
+ const fmt = (amount) => formatUnits(amount, USDC_DECIMALS);
33
+ const parse = (amount) => parseUnits(amount, USDC_DECIMALS);
34
+ const STATUS_NAMES = ["ACTIVE", "REVOKED", "EXPIRED", "CHALLENGED"];
35
+ const CLASS_NAMES = ["NONE", "C1", "C2", "C3"];
36
+ const CHALLENGE_TYPE_NAMES = [
37
+ "RESERVE_SHORTFALL", "CONSTRAINT_BYPASS", "FALSE_INDEPENDENCE",
38
+ "INVALID_VERIFICATION", "SCOPE_NOT_PERFORMED",
39
+ ];
40
+ const CHALLENGE_STATUS_NAMES = ["PENDING", "UPHELD", "REJECTED", "INFORMATIONAL"];
41
+ const ERC20_ABI = [
42
+ {
43
+ name: "balanceOf", type: "function",
44
+ inputs: [{ name: "account", type: "address" }],
45
+ outputs: [{ type: "uint256" }],
46
+ stateMutability: "view",
47
+ },
48
+ {
49
+ name: "approve", type: "function",
50
+ inputs: [{ name: "spender", type: "address" }, { name: "amount", type: "uint256" }],
51
+ outputs: [{ type: "bool" }],
52
+ stateMutability: "nonpayable",
53
+ },
54
+ ];
55
+ function getActorAddresses() {
56
+ return {
57
+ operator: privateKeyToAccount(keys.operator).address,
58
+ auditor: privateKeyToAccount(keys.auditor).address,
59
+ agent: privateKeyToAccount(keys.agent).address,
60
+ ledger: privateKeyToAccount(keys.ledger).address,
61
+ };
62
+ }
63
+ // ─── MCP Server ───
64
+ const server = new McpServer({
65
+ name: "ccp",
66
+ version: "0.1.0",
67
+ });
68
+ // ─── Tool: status ───
69
+ server.tool("ccp_status", "Get full CCP protocol status: spending limits, reserve vault, balances, and active certificate", {}, async () => {
70
+ const actors = getActorAddresses();
71
+ const [maxSingle, maxPeriodic, cosignThreshold, ledgerCosigner, remaining] = await Promise.all([
72
+ publicClient.readContract({ address: addresses.spendingLimit, abi: SpendingLimitABI, functionName: "maxSingleAction" }),
73
+ publicClient.readContract({ address: addresses.spendingLimit, abi: SpendingLimitABI, functionName: "maxPeriodicLoss" }),
74
+ publicClient.readContract({ address: addresses.spendingLimit, abi: SpendingLimitABI, functionName: "cosignThreshold" }),
75
+ publicClient.readContract({ address: addresses.spendingLimit, abi: SpendingLimitABI, functionName: "ledgerCosigner" }),
76
+ publicClient.readContract({ address: addresses.spendingLimit, abi: SpendingLimitABI, functionName: "getRemainingAllowance" }),
77
+ ]);
78
+ const [spent, limit, periodEnd] = await publicClient.readContract({
79
+ address: addresses.spendingLimit, abi: SpendingLimitABI, functionName: "getSpentInPeriod",
80
+ });
81
+ const [reserveBalance, isLocked, lockUntil, statedAmount] = await Promise.all([
82
+ publicClient.readContract({ address: addresses.reserveVault, abi: ReserveVaultABI, functionName: "getReserveBalance" }),
83
+ publicClient.readContract({ address: addresses.reserveVault, abi: ReserveVaultABI, functionName: "isLocked" }),
84
+ publicClient.readContract({ address: addresses.reserveVault, abi: ReserveVaultABI, functionName: "lockUntil" }),
85
+ publicClient.readContract({ address: addresses.reserveVault, abi: ReserveVaultABI, functionName: "getStatedAmount" }),
86
+ ]);
87
+ // Balances
88
+ const balances = {};
89
+ for (const [name, addr] of Object.entries(actors)) {
90
+ const [hbar, usdc] = await Promise.all([
91
+ publicClient.getBalance({ address: addr }),
92
+ publicClient.readContract({ address: addresses.usdc, abi: ERC20_ABI, functionName: "balanceOf", args: [addr] }),
93
+ ]);
94
+ balances[name] = { hbar: formatEther(hbar), usdc: fmt(usdc) };
95
+ }
96
+ // Active cert
97
+ let certificate = null;
98
+ try {
99
+ const certHash = await publicClient.readContract({
100
+ address: addresses.registry, abi: CCPRegistryABI, functionName: "getActiveCertificate", args: [actors.agent],
101
+ });
102
+ if (certHash !== "0x0000000000000000000000000000000000000000000000000000000000000000") {
103
+ const cert = await publicClient.readContract({
104
+ address: addresses.registry, abi: CCPRegistryABI, functionName: "getCertificate", args: [certHash],
105
+ });
106
+ const isValid = await publicClient.readContract({
107
+ address: addresses.registry, abi: CCPRegistryABI, functionName: "isValid", args: [certHash],
108
+ });
109
+ certificate = {
110
+ certHash,
111
+ class: CLASS_NAMES[cert.certificateClass] || "Unknown",
112
+ status: STATUS_NAMES[cert.status] || "Unknown",
113
+ containmentBound: `$${fmt(cert.containmentBound)} USDC`,
114
+ expires: new Date(Number(cert.expiresAt) * 1000).toISOString(),
115
+ valid: isValid,
116
+ };
117
+ }
118
+ }
119
+ catch { }
120
+ return {
121
+ content: [{
122
+ type: "text",
123
+ text: JSON.stringify({
124
+ spendingLimit: {
125
+ maxSingleAction: `$${fmt(maxSingle)} USDC`,
126
+ maxPeriodicLoss: `$${fmt(maxPeriodic)} USDC`,
127
+ cosignThreshold: `$${fmt(cosignThreshold)} USDC`,
128
+ ledgerCosigner,
129
+ periodSpent: `$${fmt(spent)} / $${fmt(limit)} USDC`,
130
+ remainingAllowance: `$${fmt(remaining)} USDC`,
131
+ periodEnds: new Date(Number(periodEnd) * 1000).toISOString(),
132
+ },
133
+ reserveVault: {
134
+ balance: `$${fmt(reserveBalance)} USDC`,
135
+ statedAmount: `$${fmt(statedAmount)} USDC`,
136
+ locked: isLocked,
137
+ lockUntil: Number(lockUntil) > 0 ? new Date(Number(lockUntil) * 1000).toISOString() : null,
138
+ },
139
+ balances,
140
+ certificate,
141
+ }, null, 2),
142
+ }],
143
+ };
144
+ });
145
+ // ─── Tool: addresses ───
146
+ server.tool("ccp_addresses", "Get all CCP contract addresses and network config", {}, async () => ({
147
+ content: [{
148
+ type: "text",
149
+ text: JSON.stringify({
150
+ contracts: {
151
+ registry: addresses.registry,
152
+ reserveVault: addresses.reserveVault,
153
+ spendingLimit: addresses.spendingLimit,
154
+ auditorStaking: addresses.auditorStaking,
155
+ feeEscrow: addresses.feeEscrow,
156
+ challengeManager: addresses.challengeManager,
157
+ usdc: addresses.usdc,
158
+ },
159
+ network: {
160
+ rpcUrl: hederaConfig.rpcUrl,
161
+ chainId: hederaConfig.chainId,
162
+ network: hederaConfig.network,
163
+ hcsTopicId: hederaConfig.hcsTopicId || null,
164
+ },
165
+ actors: getActorAddresses(),
166
+ }, null, 2),
167
+ }],
168
+ }));
169
+ // ─── Tool: cert:verify ───
170
+ server.tool("ccp_cert_verify", "Verify if an agent meets containment requirements. Returns whether the agent is acceptable for transacting.", {
171
+ agentAddress: z.string().describe("The agent's Ethereum address to verify"),
172
+ minClass: z.number().default(1).describe("Minimum certificate class required (1=C1, 2=C2, 3=C3)"),
173
+ maxLoss: z.string().default("100000").describe("Maximum acceptable loss in USDC (e.g. '100000')"),
174
+ }, async ({ agentAddress, minClass, maxLoss }) => {
175
+ const maxLossUsdc = parse(maxLoss);
176
+ const [acceptable, certHash] = await publicClient.readContract({
177
+ address: addresses.registry, abi: CCPRegistryABI, functionName: "verify",
178
+ args: [agentAddress, minClass, maxLossUsdc],
179
+ });
180
+ return {
181
+ content: [{
182
+ type: "text",
183
+ text: JSON.stringify({
184
+ agent: agentAddress,
185
+ minClassRequired: `C${minClass}`,
186
+ maxAcceptableLoss: `$${maxLoss} USDC`,
187
+ acceptable,
188
+ certHash,
189
+ result: acceptable ? "VERIFICATION PASSED" : "VERIFICATION FAILED",
190
+ }, null, 2),
191
+ }],
192
+ };
193
+ });
194
+ // ─── Tool: cert:get ───
195
+ server.tool("ccp_cert_get", "Get full certificate details by cert hash", { certHash: z.string().describe("The certificate hash") }, async ({ certHash }) => {
196
+ const cert = await publicClient.readContract({
197
+ address: addresses.registry, abi: CCPRegistryABI, functionName: "getCertificate", args: [certHash],
198
+ });
199
+ const isValid = await publicClient.readContract({
200
+ address: addresses.registry, abi: CCPRegistryABI, functionName: "isValid", args: [certHash],
201
+ });
202
+ return {
203
+ content: [{
204
+ type: "text",
205
+ text: JSON.stringify({
206
+ certHash,
207
+ operator: cert.operator,
208
+ agent: cert.agent,
209
+ class: CLASS_NAMES[cert.certificateClass] || "Unknown",
210
+ status: STATUS_NAMES[cert.status] || "Unknown",
211
+ valid: isValid,
212
+ containmentBound: `$${fmt(cert.containmentBound)} USDC`,
213
+ issuedAt: new Date(Number(cert.issuedAt) * 1000).toISOString(),
214
+ expiresAt: new Date(Number(cert.expiresAt) * 1000).toISOString(),
215
+ reserveVault: cert.reserveVault,
216
+ spendingLimit: cert.spendingLimit,
217
+ ipfsUri: cert.ipfsUri,
218
+ auditors: cert.auditors || [],
219
+ }, null, 2),
220
+ }],
221
+ };
222
+ });
223
+ // ─── Tool: cert:lookup ───
224
+ server.tool("ccp_cert_lookup", "Look up the active certificate for an agent by their address", { agentAddress: z.string().describe("The agent's Ethereum address") }, async ({ agentAddress }) => {
225
+ const certHash = await publicClient.readContract({
226
+ address: addresses.registry, abi: CCPRegistryABI, functionName: "getActiveCertificate",
227
+ args: [agentAddress],
228
+ });
229
+ const empty = certHash === "0x0000000000000000000000000000000000000000000000000000000000000000";
230
+ return {
231
+ content: [{
232
+ type: "text",
233
+ text: JSON.stringify({
234
+ agent: agentAddress,
235
+ activeCertHash: empty ? null : certHash,
236
+ hasCertificate: !empty,
237
+ }, null, 2),
238
+ }],
239
+ };
240
+ });
241
+ // ─── Tool: cert:publish ───
242
+ server.tool("ccp_cert_publish", "Publish a new containment certificate. Runs the full flow: auditor audit + stake + attest, operator Ledger sign, on-chain publish.", {}, async () => {
243
+ const operatorWallet = getWalletClient(keys.operator);
244
+ const agentWallet = getWalletClient(keys.agent);
245
+ const containmentBound = 50000000000n;
246
+ const auditorStake = 1500000000n;
247
+ const certHash = keccak256(encodePacked(["address", "address", "uint256", "string"], [agentWallet.account.address, operatorWallet.account.address, BigInt(Date.now()), "ccp-v0.2"]));
248
+ // Phase 1: Auditor
249
+ const { signature: auditorSig, auditResult } = await attestCertificate(certHash, addresses.spendingLimit, addresses.reserveVault, containmentBound, auditorStake);
250
+ // Phase 2: Operator sign
251
+ const operatorSig = await ledgerSignCertificate(certHash);
252
+ // Phase 3: Publish
253
+ const publishParams = {
254
+ certHash,
255
+ agent: agentWallet.account.address,
256
+ certificateClass: 2,
257
+ expiresAt: Math.floor(Date.now() / 1000) + 60 * 24 * 3600,
258
+ containmentBound,
259
+ reserveVault: addresses.reserveVault,
260
+ spendingLimit: addresses.spendingLimit,
261
+ ipfsUri: "ipfs://QmCCPDemoCertificate",
262
+ };
263
+ const tx = await operatorWallet.writeContract({
264
+ address: addresses.registry, abi: CCPRegistryABI, functionName: "publish",
265
+ args: [publishParams, operatorSig, [auditorSig]],
266
+ });
267
+ await publicClient.waitForTransactionReceipt({ hash: tx });
268
+ // HCS event
269
+ try {
270
+ await publishCertificatePublished(certHash, agentWallet.account.address, operatorWallet.account.address, "C2", fmt(containmentBound));
271
+ }
272
+ catch { }
273
+ const isValid = await publicClient.readContract({
274
+ address: addresses.registry, abi: CCPRegistryABI, functionName: "isValid", args: [certHash],
275
+ });
276
+ return {
277
+ content: [{
278
+ type: "text",
279
+ text: JSON.stringify({
280
+ certHash,
281
+ txHash: tx,
282
+ agent: agentWallet.account.address,
283
+ operator: operatorWallet.account.address,
284
+ class: "C2",
285
+ containmentBound: "$50,000 USDC",
286
+ valid: isValid,
287
+ auditClass: auditResult.certClass,
288
+ }, null, 2),
289
+ }],
290
+ };
291
+ });
292
+ // ─── Tool: cert:revoke ───
293
+ server.tool("ccp_cert_revoke", "Revoke an active certificate (operator only)", { certHash: z.string().describe("The certificate hash to revoke") }, async ({ certHash }) => {
294
+ const operatorWallet = getWalletClient(keys.operator);
295
+ const tx = await operatorWallet.writeContract({
296
+ address: addresses.registry, abi: CCPRegistryABI, functionName: "revoke", args: [certHash],
297
+ });
298
+ await publicClient.waitForTransactionReceipt({ hash: tx });
299
+ return {
300
+ content: [{
301
+ type: "text",
302
+ text: JSON.stringify({ certHash, txHash: tx, status: "REVOKED" }, null, 2),
303
+ }],
304
+ };
305
+ });
306
+ // ─── Tool: reserve:status ───
307
+ server.tool("ccp_reserve_status", "Get reserve vault status: balance, lock, adequacy for C2/C3", {}, async () => {
308
+ const [balance, statedAmount, isLocked, lockUntil, isAdequateC2, isAdequateC3] = await Promise.all([
309
+ publicClient.readContract({ address: addresses.reserveVault, abi: ReserveVaultABI, functionName: "getReserveBalance" }),
310
+ publicClient.readContract({ address: addresses.reserveVault, abi: ReserveVaultABI, functionName: "getStatedAmount" }),
311
+ publicClient.readContract({ address: addresses.reserveVault, abi: ReserveVaultABI, functionName: "isLocked" }),
312
+ publicClient.readContract({ address: addresses.reserveVault, abi: ReserveVaultABI, functionName: "lockUntil" }),
313
+ publicClient.readContract({ address: addresses.reserveVault, abi: ReserveVaultABI, functionName: "isAdequate", args: [50000000000n, 30000] }),
314
+ publicClient.readContract({ address: addresses.reserveVault, abi: ReserveVaultABI, functionName: "isAdequate", args: [50000000000n, 50000] }),
315
+ ]);
316
+ return {
317
+ content: [{
318
+ type: "text",
319
+ text: JSON.stringify({
320
+ balance: `$${fmt(balance)} USDC`,
321
+ statedAmount: `$${fmt(statedAmount)} USDC`,
322
+ locked: isLocked,
323
+ lockUntil: Number(lockUntil) > 0 ? new Date(Number(lockUntil) * 1000).toISOString() : null,
324
+ adequateForC2: isAdequateC2,
325
+ adequateForC3: isAdequateC3,
326
+ }, null, 2),
327
+ }],
328
+ };
329
+ });
330
+ // ─── Tool: reserve:deposit ───
331
+ server.tool("ccp_reserve_deposit", "Deposit USDC into the reserve vault (operator)", { amount: z.string().describe("Amount in USDC to deposit (e.g. '150000')") }, async ({ amount }) => {
332
+ const amountParsed = parse(amount);
333
+ const operatorWallet = getWalletClient(keys.operator);
334
+ const approveTx = await operatorWallet.writeContract({
335
+ address: addresses.usdc, abi: ERC20_ABI, functionName: "approve", args: [addresses.reserveVault, amountParsed],
336
+ });
337
+ await publicClient.waitForTransactionReceipt({ hash: approveTx });
338
+ const depositTx = await operatorWallet.writeContract({
339
+ address: addresses.reserveVault, abi: ReserveVaultABI, functionName: "deposit", args: [amountParsed],
340
+ });
341
+ await publicClient.waitForTransactionReceipt({ hash: depositTx });
342
+ const newBalance = await publicClient.readContract({
343
+ address: addresses.reserveVault, abi: ReserveVaultABI, functionName: "getReserveBalance",
344
+ });
345
+ return {
346
+ content: [{
347
+ type: "text",
348
+ text: JSON.stringify({
349
+ deposited: `$${amount} USDC`,
350
+ approveTxHash: approveTx,
351
+ depositTxHash: depositTx,
352
+ newBalance: `$${fmt(newBalance)} USDC`,
353
+ }, null, 2),
354
+ }],
355
+ };
356
+ });
357
+ // ─── Tool: reserve:lock ───
358
+ server.tool("ccp_reserve_lock", "Lock the reserve vault for N days (operator)", { days: z.number().describe("Number of days to lock the reserve") }, async ({ days }) => {
359
+ const lockUntil = Math.floor(Date.now() / 1000) + days * 24 * 3600;
360
+ const operatorWallet = getWalletClient(keys.operator);
361
+ const tx = await operatorWallet.writeContract({
362
+ address: addresses.reserveVault, abi: ReserveVaultABI, functionName: "lock", args: [lockUntil],
363
+ });
364
+ await publicClient.waitForTransactionReceipt({ hash: tx });
365
+ return {
366
+ content: [{
367
+ type: "text",
368
+ text: JSON.stringify({
369
+ txHash: tx,
370
+ lockUntil: new Date(lockUntil * 1000).toISOString(),
371
+ days,
372
+ }, null, 2),
373
+ }],
374
+ };
375
+ });
376
+ // ─── Tool: spending:status ───
377
+ server.tool("ccp_spending_status", "Get spending limit configuration and current period tracking", {}, async () => {
378
+ const [maxSingle, maxPeriodic, cosignThreshold, ledgerCosigner, remaining, periodDuration] = await Promise.all([
379
+ publicClient.readContract({ address: addresses.spendingLimit, abi: SpendingLimitABI, functionName: "maxSingleAction" }),
380
+ publicClient.readContract({ address: addresses.spendingLimit, abi: SpendingLimitABI, functionName: "maxPeriodicLoss" }),
381
+ publicClient.readContract({ address: addresses.spendingLimit, abi: SpendingLimitABI, functionName: "cosignThreshold" }),
382
+ publicClient.readContract({ address: addresses.spendingLimit, abi: SpendingLimitABI, functionName: "ledgerCosigner" }),
383
+ publicClient.readContract({ address: addresses.spendingLimit, abi: SpendingLimitABI, functionName: "getRemainingAllowance" }),
384
+ publicClient.readContract({ address: addresses.spendingLimit, abi: SpendingLimitABI, functionName: "periodDuration" }),
385
+ ]);
386
+ const [spent, limit, periodEnd] = await publicClient.readContract({
387
+ address: addresses.spendingLimit, abi: SpendingLimitABI, functionName: "getSpentInPeriod",
388
+ });
389
+ return {
390
+ content: [{
391
+ type: "text",
392
+ text: JSON.stringify({
393
+ maxSingleAction: `$${fmt(maxSingle)} USDC`,
394
+ maxPeriodicLoss: `$${fmt(maxPeriodic)} USDC`,
395
+ cosignThreshold: `$${fmt(cosignThreshold)} USDC`,
396
+ ledgerCosigner,
397
+ periodDuration: `${Number(periodDuration) / 3600} hours`,
398
+ periodSpent: `$${fmt(spent)} USDC`,
399
+ periodLimit: `$${fmt(limit)} USDC`,
400
+ remainingAllowance: `$${fmt(remaining)} USDC`,
401
+ periodEnds: new Date(Number(periodEnd) * 1000).toISOString(),
402
+ rules: {
403
+ belowThreshold: "Agent-only signature (no Ledger needed)",
404
+ aboveThreshold: "Requires Ledger co-signature",
405
+ aboveMaxSingle: "BLOCKED (absolute limit, cannot be bypassed)",
406
+ exceedsPeriodic: "BLOCKED (period limit, resets after period duration)",
407
+ },
408
+ }, null, 2),
409
+ }],
410
+ };
411
+ });
412
+ // ─── Tool: spending:pay ───
413
+ server.tool("ccp_spending_pay", "Execute a payment through SpendingLimit (agent-only signature, for amounts below cosign threshold of $5,000)", {
414
+ to: z.string().describe("Recipient address"),
415
+ amount: z.string().describe("Amount in USDC (e.g. '500')"),
416
+ }, async ({ to, amount }) => {
417
+ const amountParsed = parse(amount);
418
+ const agentWallet = getWalletClient(keys.agent);
419
+ try {
420
+ const tx = await agentWallet.writeContract({
421
+ address: addresses.spendingLimit, abi: SpendingLimitABI, functionName: "execute",
422
+ args: [to, amountParsed, "0x"],
423
+ });
424
+ await publicClient.waitForTransactionReceipt({ hash: tx });
425
+ const [spent, limit] = await publicClient.readContract({
426
+ address: addresses.spendingLimit, abi: SpendingLimitABI, functionName: "getSpentInPeriod",
427
+ });
428
+ try {
429
+ await publishAgentTransaction(agentWallet.account.address, to, amount, false, fmt(spent), fmt(limit));
430
+ }
431
+ catch { }
432
+ return {
433
+ content: [{
434
+ type: "text",
435
+ text: JSON.stringify({
436
+ txHash: tx,
437
+ amount: `$${amount} USDC`,
438
+ to,
439
+ cosigned: false,
440
+ periodSpent: `$${fmt(spent)} / $${fmt(limit)} USDC`,
441
+ }, null, 2),
442
+ }],
443
+ };
444
+ }
445
+ catch (error) {
446
+ return {
447
+ content: [{
448
+ type: "text",
449
+ text: JSON.stringify({
450
+ error: "TRANSACTION_FAILED",
451
+ reason: error.message?.slice(0, 200),
452
+ amount: `$${amount} USDC`,
453
+ to,
454
+ }, null, 2),
455
+ }],
456
+ isError: true,
457
+ };
458
+ }
459
+ });
460
+ // ─── Tool: spending:pay:cosign ───
461
+ server.tool("ccp_spending_pay_cosign", "Execute a payment with Ledger co-signature (for amounts above cosign threshold of $5,000, up to $10,000 max single action)", {
462
+ to: z.string().describe("Recipient address"),
463
+ amount: z.string().describe("Amount in USDC (e.g. '7000')"),
464
+ }, async ({ to, amount }) => {
465
+ const amountParsed = parse(amount);
466
+ const agentWallet = getWalletClient(keys.agent);
467
+ try {
468
+ const tx = await executeWithLedgerCosign(to, amountParsed, addresses.spendingLimit);
469
+ const [spent, limit] = await publicClient.readContract({
470
+ address: addresses.spendingLimit, abi: SpendingLimitABI, functionName: "getSpentInPeriod",
471
+ });
472
+ try {
473
+ await publishAgentTransaction(agentWallet.account.address, to, amount, true, fmt(spent), fmt(limit));
474
+ }
475
+ catch { }
476
+ return {
477
+ content: [{
478
+ type: "text",
479
+ text: JSON.stringify({
480
+ txHash: tx,
481
+ amount: `$${amount} USDC`,
482
+ to,
483
+ cosigned: true,
484
+ periodSpent: `$${fmt(spent)} / $${fmt(limit)} USDC`,
485
+ }, null, 2),
486
+ }],
487
+ };
488
+ }
489
+ catch (error) {
490
+ const [spent] = await publicClient.readContract({
491
+ address: addresses.spendingLimit, abi: SpendingLimitABI, functionName: "getSpentInPeriod",
492
+ });
493
+ try {
494
+ await publishTransactionBlocked(agentWallet.account.address, amount, "EXCEEDS_LIMIT", fmt(spent), "50000");
495
+ }
496
+ catch { }
497
+ return {
498
+ content: [{
499
+ type: "text",
500
+ text: JSON.stringify({
501
+ error: "TRANSACTION_BLOCKED",
502
+ reason: error.message?.slice(0, 200),
503
+ amount: `$${amount} USDC`,
504
+ to,
505
+ containmentHeld: true,
506
+ }, null, 2),
507
+ }],
508
+ isError: true,
509
+ };
510
+ }
511
+ });
512
+ // ─── Tool: auditor:status ───
513
+ server.tool("ccp_auditor_status", "Get auditor record: attestation count, active stake, challenge history", { address: z.string().optional().describe("Auditor address (defaults to configured auditor)") }, async ({ address: addr }) => {
514
+ const auditorAddr = (addr || privateKeyToAccount(keys.auditor).address);
515
+ const [record, totalStaked, balance] = await Promise.all([
516
+ publicClient.readContract({ address: addresses.auditorStaking, abi: AuditorStakingABI, functionName: "getAuditorRecord", args: [auditorAddr] }),
517
+ publicClient.readContract({ address: addresses.auditorStaking, abi: AuditorStakingABI, functionName: "getTotalStaked", args: [auditorAddr] }),
518
+ publicClient.readContract({ address: addresses.usdc, abi: ERC20_ABI, functionName: "balanceOf", args: [auditorAddr] }),
519
+ ]);
520
+ return {
521
+ content: [{
522
+ type: "text",
523
+ text: JSON.stringify({
524
+ auditor: auditorAddr,
525
+ totalAttestations: Number(record.totalAttestations),
526
+ successfulChallenges: Number(record.successfulChallenges),
527
+ activeStake: `$${fmt(record.activeStake)} USDC`,
528
+ totalStaked: `$${fmt(totalStaked)} USDC`,
529
+ usdcBalance: `$${fmt(balance)} USDC`,
530
+ }, null, 2),
531
+ }],
532
+ };
533
+ });
534
+ // ─── Tool: auditor:audit ───
535
+ server.tool("ccp_auditor_audit", "Run a read-only containment audit — checks spending limits, Ledger cosigner, reserve adequacy, and lock status", {}, async () => {
536
+ const [maxSingle, maxPeriodic, cosignThreshold, ledgerCosigner, reserveBalance, reserveAdequate, reserveLocked] = await Promise.all([
537
+ publicClient.readContract({ address: addresses.spendingLimit, abi: SpendingLimitABI, functionName: "maxSingleAction" }),
538
+ publicClient.readContract({ address: addresses.spendingLimit, abi: SpendingLimitABI, functionName: "maxPeriodicLoss" }),
539
+ publicClient.readContract({ address: addresses.spendingLimit, abi: SpendingLimitABI, functionName: "cosignThreshold" }),
540
+ publicClient.readContract({ address: addresses.spendingLimit, abi: SpendingLimitABI, functionName: "ledgerCosigner" }),
541
+ publicClient.readContract({ address: addresses.reserveVault, abi: ReserveVaultABI, functionName: "getReserveBalance" }),
542
+ publicClient.readContract({ address: addresses.reserveVault, abi: ReserveVaultABI, functionName: "isAdequate", args: [50000000000n, 30000] }),
543
+ publicClient.readContract({ address: addresses.reserveVault, abi: ReserveVaultABI, functionName: "isLocked" }),
544
+ ]);
545
+ const hasLedger = ledgerCosigner !== "0x0000000000000000000000000000000000000000";
546
+ const findings = {
547
+ maxSingleAction: `$${fmt(maxSingle)} USDC`,
548
+ maxPeriodicLoss: `$${fmt(maxPeriodic)} USDC`,
549
+ cosignThreshold: `$${fmt(cosignThreshold)} USDC`,
550
+ ledgerCosignerSet: hasLedger,
551
+ ledgerCosigner: ledgerCosigner,
552
+ reserveBalance: `$${fmt(reserveBalance)} USDC`,
553
+ reserveAdequateForC2: reserveAdequate,
554
+ reserveLocked: reserveLocked,
555
+ };
556
+ const passed = hasLedger && reserveAdequate && reserveLocked;
557
+ return {
558
+ content: [{
559
+ type: "text",
560
+ text: JSON.stringify({
561
+ findings,
562
+ passed,
563
+ recommendedClass: passed ? "C2" : "INSUFFICIENT",
564
+ message: passed
565
+ ? "All containment checks passed. Agent-independent constraints verified."
566
+ : "Audit failed. Check ledger, reserve adequacy, and lock status.",
567
+ }, null, 2),
568
+ }],
569
+ };
570
+ });
571
+ // ─── Tool: challenge:get ───
572
+ server.tool("ccp_challenge_get", "Get details of a specific challenge by ID", { challengeId: z.string().describe("The challenge ID") }, async ({ challengeId }) => {
573
+ const challenge = await publicClient.readContract({
574
+ address: addresses.challengeManager, abi: ChallengeManagerABI, functionName: "getChallenge",
575
+ args: [BigInt(challengeId)],
576
+ });
577
+ return {
578
+ content: [{
579
+ type: "text",
580
+ text: JSON.stringify({
581
+ challengeId,
582
+ certHash: challenge.certHash,
583
+ challenger: challenge.challenger,
584
+ type: CHALLENGE_TYPE_NAMES[challenge.challengeType] || "Unknown",
585
+ status: CHALLENGE_STATUS_NAMES[challenge.status] || "Unknown",
586
+ bond: `$${fmt(challenge.bond)} USDC`,
587
+ submittedAt: new Date(Number(challenge.submittedAt) * 1000).toISOString(),
588
+ resolvedAt: Number(challenge.resolvedAt) > 0 ? new Date(Number(challenge.resolvedAt) * 1000).toISOString() : null,
589
+ }, null, 2),
590
+ }],
591
+ };
592
+ });
593
+ // ─── Tool: challenge:list ───
594
+ server.tool("ccp_challenge_list", "List all challenges for a certificate", { certHash: z.string().describe("The certificate hash") }, async ({ certHash }) => {
595
+ const ids = await publicClient.readContract({
596
+ address: addresses.challengeManager, abi: ChallengeManagerABI, functionName: "getChallengesByCert",
597
+ args: [certHash],
598
+ });
599
+ const challenges = [];
600
+ for (const id of ids) {
601
+ const c = await publicClient.readContract({
602
+ address: addresses.challengeManager, abi: ChallengeManagerABI, functionName: "getChallenge", args: [id],
603
+ });
604
+ challenges.push({
605
+ id: String(id),
606
+ type: CHALLENGE_TYPE_NAMES[c.challengeType] || "Unknown",
607
+ status: CHALLENGE_STATUS_NAMES[c.status] || "Unknown",
608
+ bond: `$${fmt(c.bond)} USDC`,
609
+ });
610
+ }
611
+ return {
612
+ content: [{
613
+ type: "text",
614
+ text: JSON.stringify({ certHash, totalChallenges: ids.length, challenges }, null, 2),
615
+ }],
616
+ };
617
+ });
618
+ // ─── Tool: hcs:timeline ───
619
+ server.tool("ccp_hcs_timeline", "Get the HCS event timeline — all protocol events (cert published, transactions, blocks, challenges) from Hedera Consensus Service", {}, async () => {
620
+ if (!hederaConfig.hcsTopicId) {
621
+ return {
622
+ content: [{ type: "text", text: JSON.stringify({ error: "No HCS_TOPIC_ID configured" }) }],
623
+ isError: true,
624
+ };
625
+ }
626
+ const messages = await getTopicMessages(hederaConfig.hcsTopicId, 25);
627
+ const events = messages.map((msg) => {
628
+ const { timestamp: _eventTs, ...rest } = msg.content;
629
+ return {
630
+ ...rest,
631
+ sequenceNumber: msg.sequenceNumber,
632
+ timestamp: new Date(parseFloat(msg.timestamp) * 1000).toISOString(),
633
+ };
634
+ });
635
+ return {
636
+ content: [{
637
+ type: "text",
638
+ text: JSON.stringify({ topicId: hederaConfig.hcsTopicId, totalEvents: events.length, events }, null, 2),
639
+ }],
640
+ };
641
+ });
642
+ // ─── Start ───
643
+ async function main() {
644
+ const transport = new StdioServerTransport();
645
+ await server.connect(transport);
646
+ }
647
+ main().catch(console.error);
648
+ //# sourceMappingURL=mcp.js.map