@tongateway/mcp 0.5.0 → 0.7.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.
Files changed (2) hide show
  1. package/dist/index.js +242 -4
  2. package/package.json +3 -1
package/dist/index.js CHANGED
@@ -25,6 +25,24 @@ function saveToken(token) {
25
25
  catch { }
26
26
  }
27
27
  let TOKEN = loadToken();
28
+ // Compiled AgentVault contract code (embedded for security — no external dependency)
29
+ const AGENT_WALLET_CODE_HEX = 'b5ee9c7241020a01000210000114ff00f4a413f4bcf2c80b01020120020702014803060188d020d749c120915b8eb920d70b1f20821061677374bd2182106172766bbdb0218210616f776bbdb0925f03e0821005f5e10070fb0202d0d3030171b0925f03e0fa4030e20401a2ed44d0d200d31fd31fd3fffa40d3ffd31f305172c705f2e19c078020d7218040d72128821061677374ba8e2336363603d3ffd31f301036102506c8ca0015cb1f13cb1fcbff01cf16cbffcb1fc9ed54e30e0500b02882106172766bba8e2230353535702010365e22102306c8ca0015cb1f13cb1fcbff01cf16cbffcb1fc9ed54e032078210616f776bba8e1bd3ff30552306c8ca0015cb1f13cb1fcbff01cf16cbffcb1fc9ed54e05f07f2000017a0992fda89a0e3ae43ae163f0106f2db3c0801f620d70b1f82107369676ebaf2e195208308d722018308d723208020d721d31fd31fd31fed44d0d200d31fd31fd3fffa40d3ffd31f3026b3f2d1905185baf2e1915193baf2e19207f823bbf2d19408f901547098f9107029c300953051a8f91092323ae25290b1f2e19308b397f82324bcf2d19adef800a4506510470900e0470306c8ca0015cb1f13cb1fcbff01cf16cbffcb1fc9ed54f80ff40430206e91308e4c7f21d73930709421c700b38e2d01d72820761e436c20d749c008f2e19d20d74ac002f2e19d20d71d06c712c2005230b0f2d19ed74cd7393001a4e86c128407bbf2e19dd74ac000f2e19ded55e2c6472d0b';
30
+ // Local wallet storage — agent secret keys never leave the machine
31
+ const WALLETS_FILE = join(homedir(), '.tongateway', 'wallets.json');
32
+ function loadLocalWallets() {
33
+ try {
34
+ return JSON.parse(readFileSync(WALLETS_FILE, 'utf-8'));
35
+ }
36
+ catch {
37
+ return {};
38
+ }
39
+ }
40
+ function saveLocalWallet(address, agentSecretKey, walletId) {
41
+ const wallets = loadLocalWallets();
42
+ wallets[address] = { agentSecretKey, walletId };
43
+ mkdirSync(join(homedir(), '.tongateway'), { recursive: true });
44
+ writeFileSync(WALLETS_FILE, JSON.stringify(wallets, null, 2), 'utf-8');
45
+ }
28
46
  async function apiCall(path, options = {}) {
29
47
  const headers = {
30
48
  'Content-Type': 'application/json',
@@ -140,9 +158,9 @@ server.tool('get_auth_token', 'Check if the user has completed wallet authentica
140
158
  server.tool('request_transfer', 'Request a TON transfer from the wallet owner. The request will be queued and the owner must approve it via TON Connect.', {
141
159
  to: z.string().describe('Destination TON address'),
142
160
  amountNano: z.string().describe('Amount in nanoTON (1 TON = 1000000000)'),
143
- payloadBoc: z.string().optional().describe('Optional BOC-encoded payload for the transaction'),
161
+ payload: z.string().optional().describe('Optional BOC-encoded payload for the transaction'),
144
162
  stateInit: z.string().optional().describe('Optional stateInit BOC for deploying new contracts'),
145
- }, async ({ to, amountNano, payloadBoc, stateInit }) => {
163
+ }, async ({ to, amountNano, payload, stateInit }) => {
146
164
  if (!TOKEN) {
147
165
  return {
148
166
  content: [{ type: 'text', text: 'No token configured. Use request_auth first to authenticate.' }],
@@ -151,8 +169,8 @@ server.tool('request_transfer', 'Request a TON transfer from the wallet owner. T
151
169
  }
152
170
  try {
153
171
  const body = { to, amountNano };
154
- if (payloadBoc)
155
- body.payloadBoc = payloadBoc;
172
+ if (payload)
173
+ body.payload = payload;
156
174
  if (stateInit)
157
175
  body.stateInit = stateInit;
158
176
  const result = await apiCall('/v1/safe/tx/transfer', {
@@ -385,5 +403,225 @@ server.tool('get_ton_price', 'Get the current price of TON in USD and other curr
385
403
  return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
386
404
  }
387
405
  });
406
+ server.tool('deploy_agent_wallet', 'Deploy a new Agent Wallet smart contract. This creates an autonomous wallet that the agent can use to send TON without requiring approval for each transaction. DANGER: funds in this wallet can be spent by the agent without approval.', {}, async () => {
407
+ if (!TOKEN) {
408
+ return { content: [{ type: 'text', text: 'No token configured. Use request_auth first.' }], isError: true };
409
+ }
410
+ try {
411
+ // Get owner's public key
412
+ const meResult = await apiCall('/v1/auth/me');
413
+ const ownerAddress = meResult.address;
414
+ // Get owner public key from tonapi
415
+ const pubKeyRes = await fetch(`https://tonapi.io/v2/wallet/${encodeURIComponent(ownerAddress)}/get-account-public-key`);
416
+ const pubKeyData = await pubKeyRes.json();
417
+ if (!pubKeyData.public_key)
418
+ throw new Error('Could not get owner public key');
419
+ const ownerPublicKey = pubKeyData.public_key;
420
+ // Generate agent keypair on server
421
+ const deployResult = await apiCall('/v1/agent-wallet/deploy', {
422
+ method: 'POST',
423
+ body: JSON.stringify({ ownerPublicKey }),
424
+ });
425
+ const { agentPublicKey, agentSecretKey, walletId } = deployResult;
426
+ // Build stateInit using embedded compiled code
427
+ const { Cell, beginCell, Address, contractAddress, storeStateInit } = await import('@ton/core');
428
+ const code = Cell.fromBoc(Buffer.from(AGENT_WALLET_CODE_HEX, 'hex'))[0];
429
+ const ownerPubBuf = Buffer.from(ownerPublicKey, 'hex');
430
+ const agentPubBuf = Buffer.from(agentPublicKey, 'hex');
431
+ const adminAddr = Address.parse(ownerAddress);
432
+ const data = beginCell()
433
+ .storeBit(true) // signatureAllowed
434
+ .storeUint(0, 32) // seqno
435
+ .storeUint(walletId, 32) // walletId
436
+ .storeBuffer(ownerPubBuf, 32) // ownerPublicKey
437
+ .storeAddress(adminAddr) // adminAddress
438
+ .storeBuffer(agentPubBuf, 32) // agentPublicKey (set from the start)
439
+ .storeUint(Math.floor(Date.now() / 1000) + 10 * 365 * 24 * 3600, 32) // agentValidUntil (10 years)
440
+ .endCell();
441
+ const init = { code, data };
442
+ const address = contractAddress(0, init);
443
+ const stateInitCell = beginCell().store(storeStateInit(init)).endCell();
444
+ const stateInitBoc = stateInitCell.toBoc().toString('base64');
445
+ // Deploy via safe transfer (user approves)
446
+ const transferResult = await apiCall('/v1/safe/tx/transfer', {
447
+ method: 'POST',
448
+ body: JSON.stringify({
449
+ to: address.toRawString(),
450
+ amountNano: '100000000', // 0.1 TON for deployment
451
+ stateInit: stateInitBoc,
452
+ }),
453
+ });
454
+ // Register the wallet on the server
455
+ await apiCall('/v1/agent-wallet/register', {
456
+ method: 'POST',
457
+ body: JSON.stringify({
458
+ address: address.toRawString(),
459
+ agentSecretKey,
460
+ agentPublicKey,
461
+ ownerPublicKey,
462
+ walletId,
463
+ }),
464
+ });
465
+ // Save secret key locally — never leaves this machine
466
+ saveLocalWallet(address.toRawString(), agentSecretKey, walletId);
467
+ return {
468
+ content: [{
469
+ type: 'text',
470
+ text: [
471
+ 'Agent Wallet deployment requested!',
472
+ '',
473
+ `Address: ${address.toString()}`,
474
+ `Raw: ${address.toRawString()}`,
475
+ `Request ID: ${transferResult.id}`,
476
+ '',
477
+ 'Approve the deployment in your wallet app (0.1 TON).',
478
+ 'After approval, top up the wallet with funds the agent can spend.',
479
+ '',
480
+ 'WARNING: The agent can spend funds from this wallet without your approval.',
481
+ ].join('\n'),
482
+ }],
483
+ };
484
+ }
485
+ catch (e) {
486
+ return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
487
+ }
488
+ });
489
+ server.tool('execute_agent_wallet_transfer', 'Send TON directly from an Agent Wallet. No approval needed — the agent signs and broadcasts immediately. Only works with deployed agent wallets where the agent key is set.', {
490
+ walletAddress: z.string().describe('The agent wallet contract address'),
491
+ to: z.string().describe('Destination TON address'),
492
+ amountNano: z.string().describe('Amount in nanoTON'),
493
+ }, async ({ walletAddress, to, amountNano }) => {
494
+ if (!TOKEN) {
495
+ return { content: [{ type: 'text', text: 'No token configured. Use request_auth first.' }], isError: true };
496
+ }
497
+ try {
498
+ const { Cell, beginCell, Address, SendMode, external, storeMessage } = await import('@ton/core');
499
+ const { sign, keyPairFromSeed } = await import('@ton/crypto');
500
+ // Get wallet config from local storage
501
+ const localWallets = loadLocalWallets();
502
+ const localConfig = localWallets[walletAddress];
503
+ if (!localConfig)
504
+ throw new Error('Agent wallet secret key not found locally. Was it deployed from this machine?');
505
+ // Get current seqno from server
506
+ const infoResult = await apiCall(`/v1/agent-wallet/${encodeURIComponent(walletAddress)}/info`);
507
+ const seqno = infoResult.seqno;
508
+ const walletId = localConfig.walletId;
509
+ const secretKeyBuf = Buffer.from(localConfig.agentSecretKey, 'hex');
510
+ // Normalize: if 64 bytes use as-is, if 32 bytes derive from seed
511
+ let secretKey;
512
+ if (secretKeyBuf.length === 64) {
513
+ secretKey = secretKeyBuf;
514
+ }
515
+ else {
516
+ const kp = keyPairFromSeed(secretKeyBuf);
517
+ secretKey = kp.secretKey;
518
+ }
519
+ const vaultAddr = Address.parse(walletAddress);
520
+ const destAddr = Address.parse(to);
521
+ // Build transfer message
522
+ const transferMsg = beginCell()
523
+ .storeUint(0x18, 6)
524
+ .storeAddress(destAddr)
525
+ .storeCoins(BigInt(amountNano))
526
+ .storeUint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
527
+ .endCell();
528
+ // Build actions list
529
+ const sendMode = SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS;
530
+ const actionsList = beginCell()
531
+ .storeRef(beginCell().endCell()) // empty previous
532
+ .storeUint(0x0ec3c86d, 32) // action_send_msg prefix
533
+ .storeUint(sendMode, 8)
534
+ .storeRef(transferMsg)
535
+ .endCell();
536
+ // Build unsigned body
537
+ const validUntil = Math.floor(Date.now() / 1000) + 300;
538
+ const unsignedBody = beginCell()
539
+ .storeUint(0x7369676e, 32) // prefix::signed_external
540
+ .storeUint(walletId, 32)
541
+ .storeUint(validUntil, 32)
542
+ .storeUint(seqno, 32)
543
+ .storeMaybeRef(actionsList)
544
+ .endCell();
545
+ // Sign
546
+ const signature = sign(unsignedBody.hash(), secretKey);
547
+ const signedBody = beginCell()
548
+ .storeSlice(unsignedBody.beginParse())
549
+ .storeBuffer(signature)
550
+ .endCell();
551
+ // Build external message
552
+ const extMsg = external({ to: vaultAddr, body: signedBody });
553
+ const boc = beginCell().store(storeMessage(extMsg)).endCell().toBoc().toString('base64');
554
+ // Broadcast
555
+ const broadcastRes = await fetch('https://toncenter.com/api/v2/sendBoc', {
556
+ method: 'POST',
557
+ headers: { 'Content-Type': 'application/json' },
558
+ body: JSON.stringify({ boc }),
559
+ });
560
+ const broadcastData = await broadcastRes.json();
561
+ if (!broadcastData.ok) {
562
+ throw new Error(`Broadcast failed: ${broadcastData.error || JSON.stringify(broadcastData)}`);
563
+ }
564
+ const tonAmount = (BigInt(amountNano) / 1000000000n).toString() + '.' +
565
+ (BigInt(amountNano) % 1000000000n).toString().padStart(9, '0').replace(/0+$/, '');
566
+ return {
567
+ content: [{
568
+ type: 'text',
569
+ text: [
570
+ 'Transfer executed from Agent Wallet!',
571
+ '',
572
+ `From: ${walletAddress}`,
573
+ `To: ${to}`,
574
+ `Amount: ${tonAmount} TON`,
575
+ `Seqno: ${seqno}`,
576
+ '',
577
+ 'Transaction broadcast successfully. No approval was needed.',
578
+ ].join('\n'),
579
+ }],
580
+ };
581
+ }
582
+ catch (e) {
583
+ return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
584
+ }
585
+ });
586
+ server.tool('get_agent_wallet_info', 'Get info about an Agent Wallet — balance, seqno, agent key status.', {
587
+ walletAddress: z.string().optional().describe('Agent wallet address. If omitted, lists all your agent wallets.'),
588
+ }, async ({ walletAddress }) => {
589
+ if (!TOKEN) {
590
+ return { content: [{ type: 'text', text: 'No token configured. Use request_auth first.' }], isError: true };
591
+ }
592
+ try {
593
+ if (!walletAddress) {
594
+ // List all wallets
595
+ const result = await apiCall('/v1/agent-wallet/list');
596
+ const wallets = result.wallets || [];
597
+ if (!wallets.length) {
598
+ return { content: [{ type: 'text', text: 'No agent wallets found. Use deploy_agent_wallet to create one.' }] };
599
+ }
600
+ const lines = wallets.map((w) => `- ${w.address} (created ${new Date(w.createdAt).toISOString()})`);
601
+ return {
602
+ content: [{ type: 'text', text: `Agent wallets (${wallets.length}):\n${lines.join('\n')}` }],
603
+ };
604
+ }
605
+ const result = await apiCall(`/v1/agent-wallet/${encodeURIComponent(walletAddress)}/info`);
606
+ const balanceTon = (BigInt(result.balance) / 1000000000n).toString();
607
+ const balanceFrac = (BigInt(result.balance) % 1000000000n).toString().padStart(9, '0').replace(/0+$/, '') || '0';
608
+ return {
609
+ content: [{
610
+ type: 'text',
611
+ text: [
612
+ `Agent Wallet: ${result.address}`,
613
+ `Balance: ${balanceTon}.${balanceFrac} TON`,
614
+ `Status: ${result.status}`,
615
+ `Seqno: ${result.seqno}`,
616
+ `Agent Key: ${result.agentPublicKey}`,
617
+ `Created: ${new Date(result.createdAt).toISOString()}`,
618
+ ].join('\n'),
619
+ }],
620
+ };
621
+ }
622
+ catch (e) {
623
+ return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
624
+ }
625
+ });
388
626
  const transport = new StdioServerTransport();
389
627
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tongateway/mcp",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "MCP server for Agent Gateway — lets AI agents request TON blockchain transfers",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,6 +28,8 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@modelcontextprotocol/sdk": "^1.12.1",
31
+ "@ton/core": "^0.63.1",
32
+ "@ton/crypto": "^3.3.0",
31
33
  "zod": "^4.3.6"
32
34
  },
33
35
  "devDependencies": {