@portkey/ca-agent-skills 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.
@@ -0,0 +1,756 @@
1
+ #!/usr/bin/env bun
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 { getConfig } from '../../lib/config.js';
6
+ import { createWallet, getWalletByPrivateKey } from '../../lib/aelf-client.js';
7
+ import { validateRpcUrl } from '../../lib/http.js';
8
+ import { LoginType, OperationType } from '../../lib/types.js';
9
+
10
+ // Core functions
11
+ import { checkAccount, getGuardianList, getHolderInfo, getChainInfo } from '../core/account.js';
12
+ import { getTokenBalance, getTokenList, getNftCollections, getNftItems, getTokenPrice } from '../core/assets.js';
13
+ import { getVerifierServer, sendVerificationCode, verifyCode, registerWallet, recoverWallet, checkRegisterOrRecoveryStatus } from '../core/auth.js';
14
+ import { sameChainTransfer, crossChainTransfer, recoverStuckTransfer, getTransactionResult } from '../core/transfer.js';
15
+ import { addGuardian, removeGuardian } from '../core/guardian.js';
16
+ import { callContractViewMethod, managerForwardCallWithKey } from '../core/contract.js';
17
+ import { saveKeystore, unlockWallet, lockWallet, getWalletStatus, getUnlockedWallet } from '../core/keystore.js';
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Server setup
21
+ // ---------------------------------------------------------------------------
22
+
23
+ const server = new McpServer({
24
+ name: 'ca-agent-skills',
25
+ version: '1.0.0',
26
+ });
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Helpers
30
+ // ---------------------------------------------------------------------------
31
+
32
+ const CHAIN_ID = z.enum(['AELF', 'tDVV', 'tDVW']).describe('aelf chain ID');
33
+ const NETWORK = z.enum(['mainnet', 'testnet']).default('mainnet').describe('Portkey network');
34
+
35
+ function ok(data: unknown) {
36
+ return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }] };
37
+ }
38
+
39
+ function fail(err: unknown) {
40
+ const message = err instanceof Error ? err.message : String(err);
41
+ return { content: [{ type: 'text' as const, text: `[ERROR] ${message}` }], isError: true as const };
42
+ }
43
+
44
+ /** Parse a JSON string and validate against a zod schema. */
45
+ function parseJson<T>(raw: string, schema: z.ZodType<T>, label: string): T {
46
+ let parsed: unknown;
47
+ try {
48
+ parsed = JSON.parse(raw);
49
+ } catch {
50
+ throw new Error(`Invalid JSON for ${label}: ${raw.slice(0, 200)}`);
51
+ }
52
+ return schema.parse(parsed);
53
+ }
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Shared zod schemas for JSON string inputs
57
+ // ---------------------------------------------------------------------------
58
+
59
+ const CaAddressInfoSchema = z.array(z.object({
60
+ chainId: z.string(),
61
+ caAddress: z.string(),
62
+ }));
63
+
64
+ const GuardianApprovedSchema = z.array(z.object({
65
+ identifier: z.string().optional(),
66
+ identifierHash: z.string().optional(),
67
+ type: z.union([z.string(), z.number()]).optional(),
68
+ verifierId: z.string(),
69
+ verificationDoc: z.string(),
70
+ signature: z.string(),
71
+ }));
72
+
73
+ const GuardianToAddSchema = z.object({
74
+ identifierHash: z.string(),
75
+ type: z.number(),
76
+ verificationInfo: z.object({
77
+ id: z.string(),
78
+ signature: z.string(),
79
+ verificationDoc: z.string(),
80
+ }),
81
+ });
82
+
83
+ const GuardianToRemoveSchema = z.object({
84
+ identifierHash: z.string(),
85
+ type: z.number(),
86
+ verificationInfo: z.object({
87
+ id: z.string(),
88
+ }),
89
+ });
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Wallet accessor: unlocked keystore > env var fallback
93
+ // ---------------------------------------------------------------------------
94
+
95
+ function requireWallet(): ReturnType<typeof getWalletByPrivateKey> {
96
+ const unlocked = getUnlockedWallet();
97
+ if (unlocked) return unlocked.wallet;
98
+ const pk = process.env.PORTKEY_PRIVATE_KEY;
99
+ if (pk) return getWalletByPrivateKey(pk);
100
+ throw new Error(
101
+ 'Wallet not available. Either use portkey_unlock to unlock your keystore, ' +
102
+ 'or set the PORTKEY_PRIVATE_KEY environment variable.',
103
+ );
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // 1. portkey_check_account
108
+ // ---------------------------------------------------------------------------
109
+ server.registerTool(
110
+ 'portkey_check_account',
111
+ {
112
+ description: 'Check if an email address is registered in Portkey. Use when you need to determine whether a user has an existing Portkey CA wallet. Returns isRegistered boolean and originChainId.',
113
+ inputSchema: {
114
+ email: z.string().email().describe('Email address to check'),
115
+ network: NETWORK,
116
+ },
117
+ },
118
+ async ({ email, network }) => {
119
+ try {
120
+ return ok(await checkAccount(getConfig({ network }), { email }));
121
+ } catch (err) { return fail(err); }
122
+ },
123
+ );
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // 2. portkey_get_guardian_list
127
+ // ---------------------------------------------------------------------------
128
+ server.registerTool(
129
+ 'portkey_get_guardian_list',
130
+ {
131
+ description: 'Get all guardians associated with an account. Use when you need to see which guardians protect a wallet, or to prepare for login/recovery. Returns array of guardians with their types, verifiers, and login status.',
132
+ inputSchema: {
133
+ identifier: z.string().describe('Guardian identifier (email, phone, or social user ID)'),
134
+ chainId: CHAIN_ID.optional().default('AELF'),
135
+ network: NETWORK,
136
+ },
137
+ },
138
+ async ({ identifier, chainId, network }) => {
139
+ try {
140
+ return ok(await getGuardianList(getConfig({ network }), { identifier, chainId }));
141
+ } catch (err) { return fail(err); }
142
+ },
143
+ );
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // 3. portkey_get_holder_info
147
+ // ---------------------------------------------------------------------------
148
+ server.registerTool(
149
+ 'portkey_get_holder_info',
150
+ {
151
+ description: 'Get CA holder info directly from the blockchain. Use when you need authoritative on-chain data about a wallet including guardian list, manager list, and CA address. Returns HolderInfo with caHash, caAddress, guardians, and managers.',
152
+ inputSchema: {
153
+ caHash: z.string().describe('CA hash identifier'),
154
+ chainId: CHAIN_ID,
155
+ network: NETWORK,
156
+ },
157
+ },
158
+ async ({ caHash, chainId, network }) => {
159
+ try {
160
+ return ok(await getHolderInfo(getConfig({ network }), { caHash, chainId }));
161
+ } catch (err) { return fail(err); }
162
+ },
163
+ );
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // 4. portkey_get_chain_info
167
+ // ---------------------------------------------------------------------------
168
+ server.registerTool(
169
+ 'portkey_get_chain_info',
170
+ {
171
+ description: 'Get chain configuration info including RPC endpoints, CA contract addresses, and default tokens. Use when you need chain-specific configuration to make contract calls or transfers. Returns array of ChainInfo objects.',
172
+ inputSchema: {
173
+ network: NETWORK,
174
+ },
175
+ },
176
+ async ({ network }) => {
177
+ try {
178
+ return ok(await getChainInfo(getConfig({ network })));
179
+ } catch (err) { return fail(err); }
180
+ },
181
+ );
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // 5. portkey_send_code
185
+ // ---------------------------------------------------------------------------
186
+ server.registerTool(
187
+ 'portkey_send_code',
188
+ {
189
+ description: 'Send a verification code to an email address. Use as the first step in registration or login. Requires a verifierId from portkey_get_verifier. Returns verifierSessionId needed for portkey_verify_code.',
190
+ inputSchema: {
191
+ email: z.string().email().describe('Email address to send code to'),
192
+ verifierId: z.string().describe('Verifier service ID from portkey_get_verifier'),
193
+ chainId: CHAIN_ID.default('AELF'),
194
+ operationType: z.enum(['register', 'recovery', 'addGuardian', 'deleteGuardian']).describe('Operation requiring verification'),
195
+ network: NETWORK,
196
+ },
197
+ },
198
+ async ({ email, verifierId, chainId, operationType, network }) => {
199
+ const opMap: Record<string, OperationType> = {
200
+ register: OperationType.CreateCAHolder,
201
+ recovery: OperationType.SocialRecovery,
202
+ addGuardian: OperationType.AddGuardian,
203
+ deleteGuardian: OperationType.RemoveGuardian,
204
+ };
205
+ try {
206
+ return ok(await sendVerificationCode(getConfig({ network }), {
207
+ email, verifierId, chainId, operationType: opMap[operationType],
208
+ }));
209
+ } catch (err) { return fail(err); }
210
+ },
211
+ );
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // 6. portkey_verify_code
215
+ // ---------------------------------------------------------------------------
216
+ server.registerTool(
217
+ 'portkey_verify_code',
218
+ {
219
+ description: 'Verify a 6-digit code sent to an email. Use after portkey_send_code to complete verification. Returns signature and verificationDoc needed for registration or recovery.',
220
+ inputSchema: {
221
+ email: z.string().email().describe('Email address the code was sent to'),
222
+ verificationCode: z.string().length(6).describe('6-digit verification code'),
223
+ verifierId: z.string().describe('Verifier service ID'),
224
+ verifierSessionId: z.string().describe('Session ID from portkey_send_code'),
225
+ chainId: CHAIN_ID.default('AELF'),
226
+ operationType: z.enum(['register', 'recovery', 'addGuardian', 'deleteGuardian']).describe('Operation type'),
227
+ network: NETWORK,
228
+ },
229
+ },
230
+ async ({ email, verificationCode, verifierId, verifierSessionId, chainId, operationType, network }) => {
231
+ const opMap: Record<string, OperationType> = {
232
+ register: OperationType.CreateCAHolder,
233
+ recovery: OperationType.SocialRecovery,
234
+ addGuardian: OperationType.AddGuardian,
235
+ deleteGuardian: OperationType.RemoveGuardian,
236
+ };
237
+ try {
238
+ return ok(await verifyCode(getConfig({ network }), {
239
+ email, verificationCode, verifierId, verifierSessionId, chainId, operationType: opMap[operationType],
240
+ }));
241
+ } catch (err) { return fail(err); }
242
+ },
243
+ );
244
+
245
+ // ---------------------------------------------------------------------------
246
+ // 7. portkey_get_verifier
247
+ // ---------------------------------------------------------------------------
248
+ server.registerTool(
249
+ 'portkey_get_verifier',
250
+ {
251
+ description: 'Get an assigned verifier server for verification operations. Use before sending a verification code. Returns verifier id, name, and imageUrl.',
252
+ inputSchema: {
253
+ chainId: CHAIN_ID.optional().default('AELF'),
254
+ network: NETWORK,
255
+ },
256
+ },
257
+ async ({ chainId, network }) => {
258
+ try {
259
+ return ok(await getVerifierServer(getConfig({ network }), { chainId }));
260
+ } catch (err) { return fail(err); }
261
+ },
262
+ );
263
+
264
+ // ---------------------------------------------------------------------------
265
+ // 8. portkey_register
266
+ // ---------------------------------------------------------------------------
267
+ server.registerTool(
268
+ 'portkey_register',
269
+ {
270
+ description: 'Register a new Portkey CA wallet with email. Use after completing email verification (portkey_verify_code). Requires a manager address from a newly created wallet. Returns sessionId to poll with portkey_check_status.',
271
+ inputSchema: {
272
+ email: z.string().email().describe('Email address'),
273
+ manager: z.string().describe('Manager wallet address (from createWallet)'),
274
+ verifierId: z.string().describe('Verifier service ID'),
275
+ verificationDoc: z.string().describe('Verification document from portkey_verify_code'),
276
+ signature: z.string().describe('Signature from portkey_verify_code'),
277
+ chainId: CHAIN_ID.default('AELF'),
278
+ network: NETWORK,
279
+ },
280
+ },
281
+ async ({ email, manager, verifierId, verificationDoc, signature, chainId, network }) => {
282
+ try {
283
+ return ok(await registerWallet(getConfig({ network }), {
284
+ email, manager, verifierId, verificationDoc, signature, chainId,
285
+ }));
286
+ } catch (err) { return fail(err); }
287
+ },
288
+ );
289
+
290
+ // ---------------------------------------------------------------------------
291
+ // 9. portkey_recover
292
+ // ---------------------------------------------------------------------------
293
+ server.registerTool(
294
+ 'portkey_recover',
295
+ {
296
+ description: 'Recover (login to) an existing Portkey CA wallet. Use after getting enough guardian approvals. Requires guardian verification signatures. Returns sessionId to poll with portkey_check_status.',
297
+ inputSchema: {
298
+ email: z.string().email().describe('Email address'),
299
+ manager: z.string().describe('New manager wallet address'),
300
+ guardiansApproved: z.string().describe('JSON string of approved guardians array: [{ identifier, identifierHash, type, verifierId, verificationDoc, signature }]'),
301
+ chainId: CHAIN_ID.default('AELF'),
302
+ network: NETWORK,
303
+ },
304
+ },
305
+ async ({ email, manager, guardiansApproved, chainId, network }) => {
306
+ try {
307
+ const parsed = parseJson(guardiansApproved, GuardianApprovedSchema, 'guardiansApproved');
308
+ return ok(await recoverWallet(getConfig({ network }), {
309
+ email, manager, guardiansApproved: parsed, chainId,
310
+ }));
311
+ } catch (err) { return fail(err); }
312
+ },
313
+ );
314
+
315
+ // ---------------------------------------------------------------------------
316
+ // 10. portkey_check_status
317
+ // ---------------------------------------------------------------------------
318
+ server.registerTool(
319
+ 'portkey_check_status',
320
+ {
321
+ description: 'Check the status of a registration or recovery request. Use after portkey_register or portkey_recover to poll for completion. Returns status (pass/pending/fail), and caAddress + caHash when status is pass.',
322
+ inputSchema: {
323
+ sessionId: z.string().describe('Session ID from register or recover'),
324
+ type: z.enum(['register', 'recovery']).describe('Request type'),
325
+ network: NETWORK,
326
+ },
327
+ },
328
+ async ({ sessionId, type, network }) => {
329
+ try {
330
+ return ok(await checkRegisterOrRecoveryStatus(getConfig({ network }), { sessionId, type }));
331
+ } catch (err) { return fail(err); }
332
+ },
333
+ );
334
+
335
+ // ---------------------------------------------------------------------------
336
+ // 11. portkey_balance
337
+ // ---------------------------------------------------------------------------
338
+ server.registerTool(
339
+ 'portkey_balance',
340
+ {
341
+ description: 'Query the token balance of a CA address on a specific chain. Use when you need to check how many tokens a wallet holds. Returns symbol, balance (in smallest unit), decimals, and tokenContractAddress.',
342
+ inputSchema: {
343
+ caAddress: z.string().describe('CA address on the chain'),
344
+ chainId: CHAIN_ID,
345
+ symbol: z.string().describe('Token symbol, e.g. ELF'),
346
+ network: NETWORK,
347
+ },
348
+ },
349
+ async ({ caAddress, chainId, symbol, network }) => {
350
+ try {
351
+ return ok(await getTokenBalance(getConfig({ network }), { caAddress, chainId, symbol }));
352
+ } catch (err) { return fail(err); }
353
+ },
354
+ );
355
+
356
+ // ---------------------------------------------------------------------------
357
+ // 12. portkey_token_list
358
+ // ---------------------------------------------------------------------------
359
+ server.registerTool(
360
+ 'portkey_token_list',
361
+ {
362
+ description: 'Get all tokens with balances for CA addresses across chains. Use to see the full token portfolio of a wallet. Returns array of tokens with balances, prices, and USD values.',
363
+ inputSchema: {
364
+ caAddressInfos: z.string().describe('JSON array of { chainId, caAddress } objects'),
365
+ network: NETWORK,
366
+ },
367
+ },
368
+ async ({ caAddressInfos, network }) => {
369
+ try {
370
+ const parsed = parseJson(caAddressInfos, CaAddressInfoSchema, 'caAddressInfos');
371
+ return ok(await getTokenList(getConfig({ network }), { caAddressInfos: parsed }));
372
+ } catch (err) { return fail(err); }
373
+ },
374
+ );
375
+
376
+ // ---------------------------------------------------------------------------
377
+ // 13. portkey_nft_collections
378
+ // ---------------------------------------------------------------------------
379
+ server.registerTool(
380
+ 'portkey_nft_collections',
381
+ {
382
+ description: 'Get NFT collections owned by CA addresses. Use to browse NFT holdings. Returns collection names, images, and item counts.',
383
+ inputSchema: {
384
+ caAddressInfos: z.string().describe('JSON array of { chainId, caAddress } objects'),
385
+ network: NETWORK,
386
+ },
387
+ },
388
+ async ({ caAddressInfos, network }) => {
389
+ try {
390
+ const parsed = parseJson(caAddressInfos, CaAddressInfoSchema, 'caAddressInfos');
391
+ return ok(await getNftCollections(getConfig({ network }), { caAddressInfos: parsed }));
392
+ } catch (err) { return fail(err); }
393
+ },
394
+ );
395
+
396
+ // ---------------------------------------------------------------------------
397
+ // 14. portkey_nft_items
398
+ // ---------------------------------------------------------------------------
399
+ server.registerTool(
400
+ 'portkey_nft_items',
401
+ {
402
+ description: 'Get NFT items within a specific collection. Use to see individual NFTs in a collection. Returns token IDs, images, balances, and metadata.',
403
+ inputSchema: {
404
+ caAddressInfos: z.string().describe('JSON array of { chainId, caAddress } objects'),
405
+ symbol: z.string().describe('Collection symbol'),
406
+ network: NETWORK,
407
+ },
408
+ },
409
+ async ({ caAddressInfos, symbol, network }) => {
410
+ try {
411
+ const parsed = parseJson(caAddressInfos, CaAddressInfoSchema, 'caAddressInfos');
412
+ return ok(await getNftItems(getConfig({ network }), { caAddressInfos: parsed, symbol }));
413
+ } catch (err) { return fail(err); }
414
+ },
415
+ );
416
+
417
+ // ---------------------------------------------------------------------------
418
+ // 15. portkey_token_price
419
+ // ---------------------------------------------------------------------------
420
+ server.registerTool(
421
+ 'portkey_token_price',
422
+ {
423
+ description: 'Get current token prices in USD. Use to check market prices. Returns array of { symbol, priceInUsd }.',
424
+ inputSchema: {
425
+ symbols: z.string().describe('Comma-separated token symbols, e.g. "ELF,USDT"'),
426
+ network: NETWORK,
427
+ },
428
+ },
429
+ async ({ symbols, network }) => {
430
+ try {
431
+ return ok(await getTokenPrice(getConfig({ network }), { symbols: symbols.split(',').map(s => s.trim()) }));
432
+ } catch (err) { return fail(err); }
433
+ },
434
+ );
435
+
436
+ // ---------------------------------------------------------------------------
437
+ // 16. portkey_transfer
438
+ // ---------------------------------------------------------------------------
439
+ server.registerTool(
440
+ 'portkey_transfer',
441
+ {
442
+ description: 'Transfer tokens on the same chain. Use when sender and receiver are on the same aelf sidechain. Requires manager private key. Returns transactionId and status.',
443
+ inputSchema: {
444
+ caHash: z.string().describe('CA hash of the sender wallet'),
445
+ tokenContractAddress: z.string().describe('Token contract address on the chain'),
446
+ symbol: z.string().describe('Token symbol, e.g. ELF'),
447
+ to: z.string().describe('Recipient address'),
448
+ amount: z.string().describe('Amount in smallest unit (e.g. 100000000 = 1 ELF)'),
449
+ memo: z.string().optional().describe('Optional transfer memo'),
450
+ chainId: CHAIN_ID,
451
+ network: NETWORK,
452
+ },
453
+ },
454
+ async ({ caHash, tokenContractAddress, symbol, to, amount, memo, chainId, network }) => {
455
+ try {
456
+ const wallet = requireWallet();
457
+ return ok(await sameChainTransfer(getConfig({ network }), wallet, {
458
+ caHash, tokenContractAddress, symbol, to, amount, memo, chainId,
459
+ }));
460
+ } catch (err) { return fail(err); }
461
+ },
462
+ );
463
+
464
+ // ---------------------------------------------------------------------------
465
+ // 17. portkey_cross_chain_transfer
466
+ // ---------------------------------------------------------------------------
467
+ server.registerTool(
468
+ 'portkey_cross_chain_transfer',
469
+ {
470
+ description: 'Transfer tokens across chains (e.g., AELF to tDVV). Two-step process handled automatically. Requires manager private key. Returns transactionId and status.',
471
+ inputSchema: {
472
+ caHash: z.string().describe('CA hash of the sender wallet'),
473
+ tokenContractAddress: z.string().describe('Token contract address on source chain'),
474
+ symbol: z.string().describe('Token symbol'),
475
+ to: z.string().describe('Recipient address on target chain'),
476
+ amount: z.string().describe('Amount in smallest unit'),
477
+ toChainId: CHAIN_ID.describe('Target chain ID'),
478
+ chainId: CHAIN_ID.describe('Source chain ID'),
479
+ network: NETWORK,
480
+ },
481
+ },
482
+ async ({ caHash, tokenContractAddress, symbol, to, amount, toChainId, chainId, network }) => {
483
+ try {
484
+ const wallet = requireWallet();
485
+ return ok(await crossChainTransfer(getConfig({ network }), wallet, {
486
+ caHash, tokenContractAddress, symbol, to, amount, toChainId, chainId,
487
+ }));
488
+ } catch (err) { return fail(err); }
489
+ },
490
+ );
491
+
492
+ // ---------------------------------------------------------------------------
493
+ // 18. portkey_tx_result
494
+ // ---------------------------------------------------------------------------
495
+ server.registerTool(
496
+ 'portkey_tx_result',
497
+ {
498
+ description: 'Get the result of a blockchain transaction. Use to check if a transaction was mined successfully. Returns full transaction result including status, logs, and block info.',
499
+ inputSchema: {
500
+ txId: z.string().describe('Transaction ID'),
501
+ chainId: CHAIN_ID,
502
+ network: NETWORK,
503
+ },
504
+ },
505
+ async ({ txId, chainId, network }) => {
506
+ try {
507
+ return ok(await getTransactionResult(getConfig({ network }), { txId, chainId }));
508
+ } catch (err) { return fail(err); }
509
+ },
510
+ );
511
+
512
+ // ---------------------------------------------------------------------------
513
+ // 19. portkey_recover_stuck_transfer
514
+ // ---------------------------------------------------------------------------
515
+ server.registerTool(
516
+ 'portkey_recover_stuck_transfer',
517
+ {
518
+ description: 'Recover tokens stuck on the Manager address after a failed cross-chain transfer. When crossChainTransfer Step 1 (CA→Manager) succeeds but Step 2 fails, tokens remain on the Manager. This tool transfers them back to the CA address.',
519
+ inputSchema: {
520
+ tokenContractAddress: z.string().describe('Token contract address'),
521
+ symbol: z.string().describe('Token symbol (e.g. ELF)'),
522
+ amount: z.string().describe('Amount in smallest unit'),
523
+ caAddress: z.string().describe('CA address to recover tokens to'),
524
+ chainId: CHAIN_ID,
525
+ memo: z.string().optional().describe('Optional memo'),
526
+ network: NETWORK,
527
+ },
528
+ },
529
+ async ({ tokenContractAddress, symbol, amount, caAddress, chainId, memo, network }) => {
530
+ try {
531
+ const wallet = requireWallet();
532
+ return ok(await recoverStuckTransfer(getConfig({ network }), wallet, {
533
+ tokenContractAddress, symbol, amount, caAddress, chainId, memo,
534
+ }));
535
+ } catch (err) { return fail(err); }
536
+ },
537
+ );
538
+
539
+ // ---------------------------------------------------------------------------
540
+ // 20. portkey_add_guardian
541
+ // ---------------------------------------------------------------------------
542
+ server.registerTool(
543
+ 'portkey_add_guardian',
544
+ {
545
+ description: 'Add a new guardian to a CA wallet. Requires existing guardian approvals. Use after verifying the new guardian identity and getting approvals from current guardians.',
546
+ inputSchema: {
547
+ caHash: z.string().describe('CA hash'),
548
+ guardianToAdd: z.string().describe('JSON: { identifierHash, type (0=Email), verificationInfo: { id, signature, verificationDoc } }'),
549
+ guardiansApproved: z.string().describe('JSON array of approved guardians'),
550
+ chainId: CHAIN_ID,
551
+ network: NETWORK,
552
+ },
553
+ },
554
+ async ({ caHash, guardianToAdd, guardiansApproved, chainId, network }) => {
555
+ try {
556
+ const wallet = requireWallet();
557
+ return ok(await addGuardian(getConfig({ network }), wallet, {
558
+ caHash,
559
+ guardianToAdd: parseJson(guardianToAdd, GuardianToAddSchema, 'guardianToAdd'),
560
+ guardiansApproved: parseJson(guardiansApproved, GuardianApprovedSchema, 'guardiansApproved'),
561
+ chainId,
562
+ }));
563
+ } catch (err) { return fail(err); }
564
+ },
565
+ );
566
+
567
+ // ---------------------------------------------------------------------------
568
+ // 21. portkey_remove_guardian
569
+ // ---------------------------------------------------------------------------
570
+ server.registerTool(
571
+ 'portkey_remove_guardian',
572
+ {
573
+ description: 'Remove a guardian from a CA wallet. Requires existing guardian approvals. The guardian must not be the only login guardian.',
574
+ inputSchema: {
575
+ caHash: z.string().describe('CA hash'),
576
+ guardianToRemove: z.string().describe('JSON: { identifierHash, type (0=Email), verificationInfo: { id } }'),
577
+ guardiansApproved: z.string().describe('JSON array of approved guardians'),
578
+ chainId: CHAIN_ID,
579
+ network: NETWORK,
580
+ },
581
+ },
582
+ async ({ caHash, guardianToRemove, guardiansApproved, chainId, network }) => {
583
+ try {
584
+ const wallet = requireWallet();
585
+ return ok(await removeGuardian(getConfig({ network }), wallet, {
586
+ caHash,
587
+ guardianToRemove: parseJson(guardianToRemove, GuardianToRemoveSchema, 'guardianToRemove'),
588
+ guardiansApproved: parseJson(guardiansApproved, GuardianApprovedSchema, 'guardiansApproved'),
589
+ chainId,
590
+ }));
591
+ } catch (err) { return fail(err); }
592
+ },
593
+ );
594
+
595
+ // ---------------------------------------------------------------------------
596
+ // 22. portkey_forward_call
597
+ // ---------------------------------------------------------------------------
598
+ server.registerTool(
599
+ 'portkey_forward_call',
600
+ {
601
+ description: 'Execute a generic ManagerForwardCall on any contract through the CA wallet. Use for any custom contract interaction. The CA contract forwards the call to the target contract on behalf of the CA address.',
602
+ inputSchema: {
603
+ caHash: z.string().describe('CA hash'),
604
+ contractAddress: z.string().describe('Target contract address'),
605
+ methodName: z.string().describe('Target method name'),
606
+ args: z.string().describe('JSON object of method arguments'),
607
+ chainId: CHAIN_ID,
608
+ network: NETWORK,
609
+ },
610
+ },
611
+ async ({ caHash, contractAddress, methodName, args, chainId, network }) => {
612
+ try {
613
+ const wallet = requireWallet();
614
+ let parsedArgs: Record<string, unknown>;
615
+ try { parsedArgs = JSON.parse(args); } catch { throw new Error(`Invalid JSON for "args": ${args.slice(0, 200)}`); }
616
+ return ok(await managerForwardCallWithKey(getConfig({ network }), wallet.privateKey, {
617
+ caHash, contractAddress, methodName, args: parsedArgs, chainId,
618
+ }));
619
+ } catch (err) { return fail(err); }
620
+ },
621
+ );
622
+
623
+ // ---------------------------------------------------------------------------
624
+ // 23. portkey_view_call
625
+ // ---------------------------------------------------------------------------
626
+ server.registerTool(
627
+ 'portkey_view_call',
628
+ {
629
+ description: 'Call a read-only (view) method on any contract. Use for querying contract state without signing. No private key needed.',
630
+ inputSchema: {
631
+ rpcUrl: z.string().describe('RPC endpoint URL'),
632
+ contractAddress: z.string().describe('Contract address'),
633
+ methodName: z.string().describe('Method name'),
634
+ params: z.string().optional().describe('JSON object of method parameters'),
635
+ network: NETWORK,
636
+ },
637
+ },
638
+ async ({ rpcUrl, contractAddress, methodName, params, network }) => {
639
+ try {
640
+ validateRpcUrl(rpcUrl);
641
+ let parsedParams: Record<string, unknown> | undefined;
642
+ if (params) { try { parsedParams = JSON.parse(params); } catch { throw new Error(`Invalid JSON for "params": ${params.slice(0, 200)}`); } }
643
+ return ok(await callContractViewMethod(getConfig({ network }), {
644
+ rpcUrl, contractAddress, methodName, params: parsedParams,
645
+ }));
646
+ } catch (err) { return fail(err); }
647
+ },
648
+ );
649
+
650
+ // ---------------------------------------------------------------------------
651
+ // Bonus: portkey_create_wallet
652
+ // ---------------------------------------------------------------------------
653
+ server.registerTool(
654
+ 'portkey_create_wallet',
655
+ {
656
+ description: 'Create a new aelf wallet (manager keypair). Use when you need a fresh manager address for registration or recovery. Returns address, privateKey, and mnemonic. IMPORTANT: after registration/recovery succeeds, use portkey_save_keystore to encrypt and persist the wallet.',
657
+ },
658
+ async () => {
659
+ try {
660
+ return ok(createWallet());
661
+ } catch (err) { return fail(err); }
662
+ },
663
+ );
664
+
665
+ // ---------------------------------------------------------------------------
666
+ // 25. portkey_save_keystore
667
+ // ---------------------------------------------------------------------------
668
+ server.registerTool(
669
+ 'portkey_save_keystore',
670
+ {
671
+ description: 'Encrypt and save the Manager wallet to a keystore file (~/.portkey/ca/). Use after registration or recovery is complete. The wallet is auto-unlocked after saving. The user must provide or confirm a password.',
672
+ inputSchema: {
673
+ password: z.string().min(1).describe('Password to encrypt the keystore'),
674
+ privateKey: z.string().describe('Manager private key (hex, from portkey_create_wallet)'),
675
+ mnemonic: z.string().describe('Manager mnemonic (from portkey_create_wallet)'),
676
+ caHash: z.string().describe('CA hash (from portkey_check_status)'),
677
+ caAddress: z.string().describe('CA address (from portkey_check_status)'),
678
+ originChainId: CHAIN_ID.default('AELF').describe('Origin chain ID where CA was created'),
679
+ network: NETWORK,
680
+ },
681
+ },
682
+ async ({ password, privateKey, mnemonic, caHash, caAddress, originChainId, network }) => {
683
+ try {
684
+ return ok(saveKeystore({
685
+ password, privateKey, mnemonic, caHash, caAddress, originChainId, network: network || 'mainnet',
686
+ }));
687
+ } catch (err) { return fail(err); }
688
+ },
689
+ );
690
+
691
+ // ---------------------------------------------------------------------------
692
+ // 26. portkey_unlock
693
+ // ---------------------------------------------------------------------------
694
+ server.registerTool(
695
+ 'portkey_unlock',
696
+ {
697
+ description: 'Unlock the encrypted keystore with a password. Loads the Manager wallet into memory for write operations. Use at the start of a new conversation if a keystore exists. Check portkey_wallet_status first to see if unlock is needed.',
698
+ inputSchema: {
699
+ password: z.string().min(1).describe('Keystore password'),
700
+ network: NETWORK,
701
+ },
702
+ },
703
+ async ({ password, network }) => {
704
+ try {
705
+ return ok(unlockWallet(password, network || 'mainnet'));
706
+ } catch (err) { return fail(err); }
707
+ },
708
+ );
709
+
710
+ // ---------------------------------------------------------------------------
711
+ // 27. portkey_lock
712
+ // ---------------------------------------------------------------------------
713
+ server.registerTool(
714
+ 'portkey_lock',
715
+ {
716
+ description: 'Lock the wallet — clear the Manager private key from memory. Use when done with write operations for security.',
717
+ },
718
+ async () => {
719
+ try {
720
+ return ok(lockWallet());
721
+ } catch (err) { return fail(err); }
722
+ },
723
+ );
724
+
725
+ // ---------------------------------------------------------------------------
726
+ // 28. portkey_wallet_status
727
+ // ---------------------------------------------------------------------------
728
+ server.registerTool(
729
+ 'portkey_wallet_status',
730
+ {
731
+ description: 'Check the wallet status: whether a keystore exists, whether it is unlocked, CA address, and manager address. Use at conversation start to determine if portkey_unlock is needed.',
732
+ inputSchema: {
733
+ network: NETWORK,
734
+ },
735
+ },
736
+ async ({ network }) => {
737
+ try {
738
+ return ok(getWalletStatus(network || 'mainnet'));
739
+ } catch (err) { return fail(err); }
740
+ },
741
+ );
742
+
743
+ // ---------------------------------------------------------------------------
744
+ // Start
745
+ // ---------------------------------------------------------------------------
746
+
747
+ async function main() {
748
+ const transport = new StdioServerTransport();
749
+ await server.connect(transport);
750
+ console.error('Portkey MCP Server running on stdio');
751
+ }
752
+
753
+ main().catch((err) => {
754
+ console.error('Fatal:', err);
755
+ process.exit(1);
756
+ });