ceaser-mcp 1.0.1 → 1.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/bin/ceaser-mcp.js CHANGED
@@ -3,13 +3,25 @@
3
3
  /**
4
4
  * Ceaser MCP Server - CLI entry point.
5
5
  *
6
- * Starts the MCP server with stdio transport for local use with Claude Code.
6
+ * Starts the MCP server with stdio transport, or runs CLI subcommands directly.
7
7
  *
8
8
  * Usage:
9
- * npx ceaser-mcp
10
- * claude mcp add --transport stdio ceaser -- npx -y ceaser-mcp
9
+ * npx ceaser-mcp Start MCP stdio server
10
+ * npx ceaser-mcp --stdio Start MCP stdio server (explicit)
11
+ * npx ceaser-mcp shield 0.001 Shield 0.001 ETH
12
+ * npx ceaser-mcp unshield <noteId> <addr> Unshield to address
13
+ * npx ceaser-mcp notes List unspent notes
14
+ * npx ceaser-mcp import <backup> Import note from backup
15
+ * npx ceaser-mcp help Print usage
11
16
  */
12
17
 
13
18
  import { startStdio } from '../src/transport/stdio.js';
19
+ import { runCli } from '../src/cli.js';
14
20
 
15
- startStdio();
21
+ const subcommand = process.argv[2];
22
+
23
+ if (subcommand && subcommand !== '--stdio') {
24
+ runCli(subcommand, process.argv.slice(3));
25
+ } else {
26
+ startStdio();
27
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ceaser-mcp",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server for Ceaser privacy protocol - shield and unshield ETH privately on Base L2",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js ADDED
@@ -0,0 +1,287 @@
1
+ /**
2
+ * CLI subcommand dispatcher for ceaser-mcp.
3
+ *
4
+ * Allows bash-only agents (e.g. OpenClaw) to shield/unshield ETH without MCP.
5
+ * All output is JSON: stdout on success, stderr on failure.
6
+ */
7
+
8
+ import { parseEther, Interface } from 'ethers';
9
+ import { randomField, toBytes32 } from './lib/crypto.js';
10
+ import { generateShieldProof, generateBurnProof } from './lib/prover.js';
11
+ import { addNote, listNotes, getNote, markSpent, exportNote, parseBackup } from './lib/noteStore.js';
12
+ import { buildTreeFromIndexer } from './tools/unshield.js';
13
+ import * as api from './lib/api.js';
14
+ import { config } from './lib/config.js';
15
+ import {
16
+ DENOMINATIONS,
17
+ ZKWRAPPER_ABI,
18
+ PROTOCOL_FEE_BPS,
19
+ BPS_DENOMINATOR,
20
+ } from './constants.js';
21
+
22
+ function outputSuccess(data) {
23
+ process.stdout.write(JSON.stringify(data, null, 2) + '\n');
24
+ process.exit(0);
25
+ }
26
+
27
+ function outputError(message) {
28
+ process.stderr.write(JSON.stringify({ error: message }, null, 2) + '\n');
29
+ process.exit(1);
30
+ }
31
+
32
+ // -- Subcommand handlers --
33
+
34
+ async function handleShield(args) {
35
+ const amount = args[0];
36
+ if (!amount) {
37
+ outputError(`Missing amount. Usage: ceaser-mcp shield <amount>\nValid denominations: ${DENOMINATIONS.map((d) => d.value).join(', ')} ETH`);
38
+ }
39
+
40
+ const denom = DENOMINATIONS.find((d) => d.value === amount);
41
+ if (!denom) {
42
+ outputError(`Invalid denomination: ${amount}. Valid: ${DENOMINATIONS.map((d) => d.value).join(', ')} ETH`);
43
+ }
44
+
45
+ const amountWei = parseEther(amount);
46
+ const secret = randomField();
47
+ const nullifier = randomField();
48
+ const assetId = 0n;
49
+
50
+ const { proofHex, commitment, commitmentHex } = await generateShieldProof(
51
+ secret, nullifier, amountWei, assetId
52
+ );
53
+
54
+ const iface = new Interface(ZKWRAPPER_ABI);
55
+ const data = iface.encodeFunctionData('shieldETH', [
56
+ proofHex,
57
+ commitmentHex,
58
+ amountWei,
59
+ ]);
60
+
61
+ const fee = (amountWei * PROTOCOL_FEE_BPS) / BPS_DENOMINATOR;
62
+ const totalValue = amountWei + fee;
63
+
64
+ const note = addNote({
65
+ secret,
66
+ nullifier,
67
+ commitment: commitment.toString(),
68
+ amount,
69
+ amountWei: amountWei.toString(),
70
+ assetId: '0',
71
+ leafIndex: null,
72
+ });
73
+
74
+ const backup = exportNote(note);
75
+
76
+ outputSuccess({
77
+ note: {
78
+ id: note.id,
79
+ amount,
80
+ amountWei: amountWei.toString(),
81
+ commitment: commitmentHex,
82
+ assetId: '0',
83
+ leafIndex: null,
84
+ },
85
+ unsignedTx: {
86
+ to: config.contractAddress,
87
+ data,
88
+ value: totalValue.toString(),
89
+ chainId: config.chainId,
90
+ },
91
+ backup,
92
+ instructions: [
93
+ 'Sign and send the unsignedTx from your wallet.',
94
+ 'The value includes the 0.25% protocol fee.',
95
+ `Total to send: ${totalValue.toString()} wei (${amount} ETH + fee).`,
96
+ 'CRITICAL: Save the backup string securely. It contains private keys for this note.',
97
+ 'After the transaction confirms, update the leaf index with: ceaser-mcp import <backup>',
98
+ ],
99
+ });
100
+ }
101
+
102
+ async function handleUnshield(args) {
103
+ const [noteId, recipient] = args;
104
+ if (!noteId || !recipient) {
105
+ outputError('Missing arguments. Usage: ceaser-mcp unshield <noteId> <recipient>');
106
+ }
107
+
108
+ if (!/^0x[0-9a-fA-F]{40}$/.test(recipient)) {
109
+ outputError('Invalid recipient address. Must be 0x-prefixed 40-char hex.');
110
+ }
111
+
112
+ const note = getNote(noteId);
113
+ if (!note) {
114
+ outputError(`Note ${noteId} not found. Run: ceaser-mcp notes`);
115
+ }
116
+
117
+ if (note.spent) {
118
+ outputError(`Note ${noteId} has already been spent.`);
119
+ }
120
+
121
+ if (note.leafIndex === null || note.leafIndex === undefined) {
122
+ outputError('Note does not have a leaf index. The shield transaction may not have confirmed yet.');
123
+ }
124
+
125
+ // Rebuild Merkle tree from indexer
126
+ process.stderr.write('[ceaser-mcp] Building Merkle tree from indexer...\n');
127
+ const tree = await buildTreeFromIndexer();
128
+
129
+ if (note.leafIndex >= tree.getLeafCount()) {
130
+ outputError(`Note leaf index ${note.leafIndex} not found in tree (tree has ${tree.getLeafCount()} leaves). Indexer may not be fully synced.`);
131
+ }
132
+
133
+ const { pathElements, pathIndices } = await tree.getProof(note.leafIndex);
134
+ const root = tree.getRoot();
135
+
136
+ // Generate burn proof
137
+ process.stderr.write('[ceaser-mcp] Generating unshield proof...\n');
138
+ const { proofHex, nullifierHashHex, rootHex } = await generateBurnProof(
139
+ note, recipient, pathElements, pathIndices, root
140
+ );
141
+
142
+ // Fee calculation
143
+ const amountWei = BigInt(note.amountWei);
144
+ const fee = (amountWei * PROTOCOL_FEE_BPS) / BPS_DENOMINATOR;
145
+ const netAmount = amountWei - fee;
146
+
147
+ // Settle via facilitator
148
+ process.stderr.write('[ceaser-mcp] Settling via facilitator...\n');
149
+ const settleResult = await api.settle(
150
+ proofHex,
151
+ nullifierHashHex,
152
+ amountWei.toString(),
153
+ (note.assetId || '0').toString(),
154
+ recipient,
155
+ rootHex
156
+ );
157
+
158
+ markSpent(noteId, settleResult.txHash);
159
+
160
+ outputSuccess({
161
+ success: true,
162
+ txHash: settleResult.txHash,
163
+ recipient,
164
+ grossAmount: amountWei.toString(),
165
+ feeWei: fee.toString(),
166
+ netAmount: netAmount.toString(),
167
+ noteId,
168
+ });
169
+ }
170
+
171
+ function handleNotes(args) {
172
+ const showAll = args.includes('--all');
173
+ const notes = listNotes(showAll);
174
+
175
+ const safeNotes = notes.map((n) => ({
176
+ id: n.id,
177
+ amount: n.amount,
178
+ amountWei: n.amountWei,
179
+ assetId: n.assetId || '0',
180
+ commitment: n.commitment,
181
+ leafIndex: n.leafIndex,
182
+ spent: n.spent,
183
+ timestamp: n.timestamp,
184
+ ...(n.spent ? { spentTxHash: n.spentTxHash, spentAt: n.spentAt } : {}),
185
+ }));
186
+
187
+ outputSuccess({
188
+ count: safeNotes.length,
189
+ notes: safeNotes,
190
+ });
191
+ }
192
+
193
+ function handleImport(args) {
194
+ const backup = args[0];
195
+ if (!backup) {
196
+ outputError('Missing backup string. Usage: ceaser-mcp import <backup>');
197
+ }
198
+
199
+ const noteData = parseBackup(backup);
200
+
201
+ // Check for duplicates
202
+ const existing = listNotes(true);
203
+ const duplicate = existing.find((n) => n.commitment === noteData.commitment);
204
+ if (duplicate) {
205
+ outputError(`Note already exists with ID ${duplicate.id} (commitment: ${duplicate.commitment})`);
206
+ }
207
+
208
+ const note = addNote({
209
+ secret: noteData.secret,
210
+ nullifier: noteData.nullifier,
211
+ commitment: noteData.commitment,
212
+ amount: noteData.amount,
213
+ amountWei: noteData.amountWei,
214
+ assetId: noteData.assetId,
215
+ leafIndex: noteData.leafIndex,
216
+ });
217
+
218
+ outputSuccess({
219
+ noteId: note.id,
220
+ amount: noteData.amount,
221
+ amountWei: noteData.amountWei,
222
+ commitment: noteData.commitment,
223
+ leafIndex: noteData.leafIndex,
224
+ assetId: noteData.assetId,
225
+ });
226
+ }
227
+
228
+ function handleHelp() {
229
+ const usage = `ceaser-mcp - Ceaser privacy protocol CLI & MCP server
230
+
231
+ USAGE:
232
+ ceaser-mcp Start MCP stdio server
233
+ ceaser-mcp shield <amount> Shield ETH (generate proof + unsigned tx)
234
+ ceaser-mcp unshield <noteId> <addr> Unshield ETH to address (gasless via x402)
235
+ ceaser-mcp notes [--all] List notes (--all includes spent)
236
+ ceaser-mcp import <backup> Import note from backup string
237
+ ceaser-mcp help Show this help
238
+
239
+ DENOMINATIONS:
240
+ ${DENOMINATIONS.map((d) => d.value).join(', ')} ETH
241
+
242
+ EXAMPLES:
243
+ ceaser-mcp shield 0.001
244
+ ceaser-mcp notes
245
+ ceaser-mcp unshield 1737000000000-a1b2c3d4 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18
246
+ ceaser-mcp import eyJzIjoiMTIzLi4uIn0=
247
+
248
+ ENVIRONMENT:
249
+ CEASER_API_URL Facilitator URL (default: https://ceaser.org)
250
+ CEASER_CONTRACT_ADDRESS zkWrapper address
251
+ CEASER_CHAIN_ID Chain ID (default: 8453)
252
+ CEASER_DATA_DIR Data directory (default: ~/.ceaser-mcp)
253
+
254
+ All CLI output is JSON (stdout=success, stderr=errors/progress).
255
+ Notes are stored at ~/.ceaser-mcp/notes.json.
256
+ `;
257
+
258
+ process.stdout.write(usage);
259
+ process.exit(0);
260
+ }
261
+
262
+ // -- Dispatcher --
263
+
264
+ const COMMANDS = {
265
+ shield: handleShield,
266
+ unshield: handleUnshield,
267
+ notes: handleNotes,
268
+ import: handleImport,
269
+ help: handleHelp,
270
+ '--help': handleHelp,
271
+ '-h': handleHelp,
272
+ };
273
+
274
+ export async function runCli(subcommand, args) {
275
+ const handler = COMMANDS[subcommand];
276
+ if (!handler) {
277
+ outputError(`Unknown command: ${subcommand}. Run: ceaser-mcp help`);
278
+ }
279
+
280
+ try {
281
+ await handler(args);
282
+ } catch (err) {
283
+ // Don't leak internal details for non-API errors
284
+ const msg = err.message?.includes('API') ? err.message : `Command failed: ${err.message}`;
285
+ outputError(msg);
286
+ }
287
+ }
@@ -20,7 +20,7 @@ import { log, logError } from '../lib/log.js';
20
20
  */
21
21
  const MAX_TREE_BATCHES = 200; // 200 * 1000 = 200k leaves max
22
22
 
23
- async function buildTreeFromIndexer() {
23
+ export async function buildTreeFromIndexer() {
24
24
  const tree = new MerkleTree();
25
25
  await tree.init();
26
26