@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.
- package/LICENSE +21 -0
- package/README.md +270 -0
- package/README.zh-CN.md +265 -0
- package/bin/platforms/claude.ts +36 -0
- package/bin/platforms/cursor.ts +39 -0
- package/bin/platforms/openclaw.ts +39 -0
- package/bin/platforms/utils.ts +144 -0
- package/bin/setup.ts +191 -0
- package/cli-helpers.ts +26 -0
- package/index.ts +145 -0
- package/lib/aelf-client.ts +281 -0
- package/lib/aelf-sdk.d.ts +103 -0
- package/lib/config.ts +46 -0
- package/lib/http.ts +197 -0
- package/lib/types.ts +492 -0
- package/mcp-config.example.json +12 -0
- package/openclaw.json +197 -0
- package/package.json +49 -0
- package/portkey_auth_skill.ts +173 -0
- package/portkey_query_skill.ts +147 -0
- package/portkey_tx_skill.ts +143 -0
- package/src/core/account.ts +230 -0
- package/src/core/assets.ts +175 -0
- package/src/core/auth.ts +310 -0
- package/src/core/contract.ts +118 -0
- package/src/core/guardian.ts +141 -0
- package/src/core/keystore.ts +319 -0
- package/src/core/transfer.ts +243 -0
- package/src/mcp/server.ts +756 -0
|
@@ -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
|
+
});
|