@tongateway/mcp 0.13.0 → 0.19.2

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/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # @tongateway/mcp
2
2
 
3
+ [![smithery badge](https://smithery.ai/badge/tongateway/agent)](https://smithery.ai/servers/tongateway/agent)
4
+
3
5
  MCP server for [Agent Gateway](https://tongateway.ai) — gives AI agents full access to the TON blockchain via Model Context Protocol.
4
6
 
5
- **14 tools:** wallet info, jettons, NFTs, transactions, transfers, .ton DNS, prices, agent wallets, and more.
7
+ **16 tools:** wallet info, jettons, NFTs, transactions, transfers, .ton DNS, prices, DEX orders, agent wallets, and more.
6
8
 
7
9
  ## Quick Start
8
10
 
@@ -50,7 +52,7 @@ openclaw config set --strict-json plugins.entries.acpx.config.mcpServers '{
50
52
  }'
51
53
  ```
52
54
 
53
- No token needed upfront — the agent authenticates via `request_auth` (generates a one-time link, user connects wallet). Token persists in `~/.tongateway/token` across restarts.
55
+ No token needed upfront — the agent authenticates via `auth.request` (generates a one-time link, user connects wallet). Token persists in `~/.tongateway/token` across restarts.
54
56
 
55
57
  ## Tools
56
58
 
@@ -58,48 +60,55 @@ No token needed upfront — the agent authenticates via `request_auth` (generate
58
60
 
59
61
  | Tool | Description |
60
62
  |------|-------------|
61
- | `request_auth` | Generate a one-time link for wallet connection |
62
- | `get_auth_token` | Retrieve token after user connects wallet |
63
+ | `auth.request` | Generate a one-time link for wallet connection |
64
+ | `auth.get_token` | Retrieve token after user connects wallet |
63
65
 
64
66
  ### Wallet
65
67
 
66
68
  | Tool | Description |
67
69
  |------|-------------|
68
- | `get_wallet_info` | Wallet address, TON balance, account status |
69
- | `get_jetton_balances` | All token balances (USDT, NOT, DOGS, etc.) |
70
- | `get_transactions` | Recent transaction history |
71
- | `get_nft_items` | NFTs owned by the wallet |
70
+ | `wallet.info` | Wallet address, TON balance, account status |
71
+ | `wallet.jettons` | All token balances (USDT, NOT, DOGS, etc.) |
72
+ | `wallet.transactions` | Recent transaction history |
73
+ | `wallet.nfts` | NFTs owned by the wallet |
72
74
 
73
75
  ### Transfers (Safe — requires wallet approval)
74
76
 
75
77
  | Tool | Description |
76
78
  |------|-------------|
77
- | `request_transfer` | Request a TON transfer (to, amountNano, payload?, stateInit?) |
78
- | `get_request_status` | Check transfer status by ID |
79
- | `list_pending_requests` | List all pending requests |
79
+ | `transfer.request` | Request a TON transfer (to, amountNano, payload?, stateInit?) |
80
+ | `transfer.status` | Check transfer status by ID |
81
+ | `transfer.pending` | List all pending requests |
80
82
 
81
83
  ### Lookup
82
84
 
83
85
  | Tool | Description |
84
86
  |------|-------------|
85
- | `resolve_name` | Resolve .ton domain to address |
86
- | `get_ton_price` | Current TON price in USD/EUR |
87
+ | `lookup.resolve_name` | Resolve .ton domain to address |
88
+ | `lookup.price` | Current TON price in USD/EUR |
89
+
90
+ ### DEX (open4dev order book)
91
+
92
+ | Tool | Description |
93
+ |------|-------------|
94
+ | `dex.create_order` | Place a limit order (fromToken, toToken, amount, price) |
95
+ | `dex.pairs` | List available trading pairs |
87
96
 
88
97
  ### Agent Wallet (Autonomous — no approval needed)
89
98
 
90
99
  | Tool | Description |
91
100
  |------|-------------|
92
- | `deploy_agent_wallet` | Deploy a dedicated wallet contract for the agent |
93
- | `execute_agent_wallet_transfer` | Send TON directly from agent wallet |
94
- | `get_agent_wallet_info` | Balance, seqno, agent key status |
101
+ | `agent_wallet.deploy` | Deploy a dedicated wallet contract for the agent |
102
+ | `agent_wallet.transfer` | Send TON directly from agent wallet |
103
+ | `agent_wallet.info` | Balance, seqno, agent key status |
95
104
 
96
105
  ## How it works
97
106
 
98
107
  ```
99
108
  You: "Send 1 TON to alice.ton"
100
109
 
101
- Agent: resolve_name("alice.ton") → 0:83df...
102
- request_transfer(to="0:83df...", amountNano="1000000000")
110
+ Agent: lookup.resolve_name("alice.ton") → 0:83df...
111
+ transfer.request(to="0:83df...", amountNano="1000000000")
103
112
  → Transfer request created. Approve in your wallet app.
104
113
  ```
105
114
 
@@ -108,10 +117,39 @@ For agent wallets (autonomous mode):
108
117
  ```
109
118
  You: "Send 0.5 TON from my agent wallet to 0:abc..."
110
119
 
111
- Agent: execute_agent_wallet_transfer(wallet, to, amount)
120
+ Agent: agent_wallet.transfer(wallet, to, amount)
112
121
  → Transfer executed. No approval needed.
113
122
  ```
114
123
 
124
+ ## Build from Source
125
+
126
+ If you prefer not to use `npx`, you can build and run locally:
127
+
128
+ ```bash
129
+ git clone https://github.com/tongateway/mcp
130
+ cd mcp
131
+ npm install
132
+ npm run build
133
+ ```
134
+
135
+ Then configure your MCP client to use the local build:
136
+
137
+ ```json
138
+ {
139
+ "mcpServers": {
140
+ "tongateway": {
141
+ "command": "node",
142
+ "args": ["/path/to/mcp/dist/index.js"],
143
+ "env": {
144
+ "AGENT_GATEWAY_API_URL": "https://api.tongateway.ai"
145
+ }
146
+ }
147
+ }
148
+ }
149
+ ```
150
+
151
+ See [SECURITY.md](SECURITY.md) for the full security model.
152
+
115
153
  ## Links
116
154
 
117
155
  - [tongateway.ai](https://tongateway.ai) — landing page + install guides
@@ -119,6 +157,8 @@ Agent: execute_agent_wallet_transfer(wallet, to, amount)
119
157
  - [API Docs](https://api.tongateway.ai/docs) — Swagger UI
120
158
  - [Agent Wallet Contract](https://github.com/tongateway/ton-agent-gateway-contract) — FunC smart contract
121
159
  - [Skill File](https://tongateway.ai/agent-gateway.md) — context file for AI agents
160
+ - [Smithery](https://smithery.ai/servers/tongateway/agent) — MCP marketplace listing
161
+ - [MCP HTTP Endpoint](https://tongateway.run.tools) — remote MCP transport
122
162
 
123
163
  ## License
124
164
 
package/dist/index.js CHANGED
@@ -1,4 +1,33 @@
1
1
  #!/usr/bin/env node
2
+ // Handle --help and --version before importing anything
3
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
4
+ console.log(`@tongateway/mcp — TON blockchain gateway for AI agents
5
+
6
+ Usage:
7
+ npx @tongateway/mcp Start MCP server (stdio transport)
8
+ npx @tongateway/mcp --http Start HTTP server (port 3100)
9
+
10
+ Environment variables:
11
+ AGENT_GATEWAY_API_URL API base URL (default: https://api.tongateway.ai)
12
+ AGENT_GATEWAY_TOKEN Pre-configured auth token (optional)
13
+ MCP_HTTP_PORT HTTP server port (default: 3100 with --http)
14
+
15
+ Tools (16):
16
+ auth.request, auth.get_token
17
+ wallet.info, wallet.jettons, wallet.transactions, wallet.nfts
18
+ transfer.request, transfer.status, transfer.pending
19
+ lookup.resolve_name, lookup.price
20
+ dex.create_order, dex.pairs
21
+ agent_wallet.deploy, agent_wallet.transfer, agent_wallet.info
22
+
23
+ Docs: https://tongateway.ai/docs
24
+ GitHub: https://github.com/tongateway/mcp`);
25
+ process.exit(0);
26
+ }
27
+ if (process.argv.includes('--version') || process.argv.includes('-v')) {
28
+ console.log('0.15.0');
29
+ process.exit(0);
30
+ }
2
31
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
32
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
33
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
@@ -69,9 +98,9 @@ const server = new McpServer({
69
98
  name: 'agent-gateway',
70
99
  version: '0.1.0',
71
100
  });
72
- server.tool('request_auth', 'Authenticate with TON blockchain. Call this FIRST if you get "No token configured" errors. Generates a one-time link ask the user to open it and connect their wallet. Then call get_auth_token to complete. Token persists across restarts.', {
101
+ server.tool('auth.request', 'Authenticate with TON blockchain. Call this tool FIRST if you get "No token configured" errors. It returns a URL that you MUST display to the user as a clickable link. The user opens it in their browser to connect their wallet. After they confirm, call auth.get_token with the authId. Do NOT use curl or fetch — use this MCP tool. Do NOT poll in a loop — just call auth.get_token once after the user says they connected.', {
73
102
  label: z.string().optional().describe('Label for this agent session (e.g. "claude-agent")'),
74
- }, async ({ label }) => {
103
+ }, { title: 'Request Authentication', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async ({ label }) => {
75
104
  try {
76
105
  const result = await fetch(`${API_URL}/v1/auth/request`, {
77
106
  method: 'POST',
@@ -86,15 +115,15 @@ server.tool('request_auth', 'Authenticate with TON blockchain. Call this FIRST i
86
115
  {
87
116
  type: 'text',
88
117
  text: [
89
- `Authentication requested.`,
118
+ `IMPORTANT: Show this link to the user NOW:`,
119
+ ``,
120
+ `👉 ${data.authUrl}`,
90
121
  ``,
91
- `Ask the user to open this link:`,
92
- data.authUrl,
122
+ `Tell the user: "Open this link and connect your wallet. Let me know when done."`,
93
123
  ``,
94
124
  `Auth ID: ${data.authId}`,
95
- `Expires: ${new Date(data.expiresAt).toISOString()}`,
96
125
  ``,
97
- `After the user connects their wallet, call get_auth_token with this authId to get the token.`,
126
+ `When the user confirms they connected, call auth.get_token with authId "${data.authId}"`,
98
127
  ].join('\n'),
99
128
  },
100
129
  ],
@@ -107,9 +136,9 @@ server.tool('request_auth', 'Authenticate with TON blockchain. Call this FIRST i
107
136
  };
108
137
  }
109
138
  });
110
- server.tool('get_auth_token', 'Complete authentication after the user opened the link from request_auth. Pass the authId you received. Once successful, all other tools become available. You only need to authenticate once — the token is saved automatically.', {
111
- authId: z.string().describe('The authId returned by request_auth'),
112
- }, async ({ authId }) => {
139
+ server.tool('auth.get_token', 'Complete authentication after the user opened the link from auth.request. Pass the authId you received. Once successful, all other tools become available. You only need to authenticate once — the token is saved automatically.', {
140
+ authId: z.string().describe('The authId returned by auth.request'),
141
+ }, { title: 'Get Auth Token', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ authId }) => {
113
142
  try {
114
143
  // Retry up to 3 times with 2s delay (KV eventual consistency)
115
144
  let data = null;
@@ -134,7 +163,7 @@ server.tool('get_auth_token', 'Complete authentication after the user opened the
134
163
  `Authentication still pending.`,
135
164
  `The user has not connected their wallet yet.`,
136
165
  ``,
137
- `Wait a moment and call get_auth_token again.`,
166
+ `Wait a moment and call auth.get_token again.`,
138
167
  ].join('\n'),
139
168
  },
140
169
  ],
@@ -152,7 +181,7 @@ server.tool('get_auth_token', 'Complete authentication after the user opened the
152
181
  `Token received and configured.`,
153
182
  `Wallet: ${data.address}`,
154
183
  ``,
155
- `You can now use request_transfer, get_request_status, and list_pending_requests.`,
184
+ `You can now use transfer.request, transfer.status, and transfer.pending.`,
156
185
  ].join('\n'),
157
186
  },
158
187
  ],
@@ -165,20 +194,23 @@ server.tool('get_auth_token', 'Complete authentication after the user opened the
165
194
  };
166
195
  }
167
196
  });
168
- server.tool('request_transfer', 'Request a TON transfer. The user must approve it in their wallet app. Amount is in nanoTON (1 TON = 1000000000). Supports optional payload (BOC) and stateInit (for contract deployment). The transfer is queued use get_request_status to check if approved.', {
169
- to: z.string().describe('Destination TON address'),
170
- amountNano: z.string().describe('Amount in nanoTON (1 TON = 1000000000)'),
171
- payload: z.string().optional().describe('Optional BOC-encoded payload for the transaction'),
172
- stateInit: z.string().optional().describe('Optional stateInit BOC for deploying new contracts'),
173
- }, async ({ to, amountNano, payload, stateInit }) => {
197
+ server.tool('transfer.request', 'Send a TON transfer. The user approves in their wallet app. Use human-readable amounts (e.g. amount="1.5"). Supports .ton domain names directly (e.g. to="alice.ton"). The API handles name resolution and decimal conversion automatically.', {
198
+ to: z.string().describe('Destination: TON address or .ton domain (e.g. "alice.ton" or "EQD...abc")'),
199
+ amount: z.string().describe('Human-readable amount (e.g. "1.5" for 1.5 TON, "100" for 100 TON)'),
200
+ comment: z.string().optional().describe('Text comment/message to attach (e.g. "Payment for services")'),
201
+ payload: z.string().optional().describe('Optional BOC-encoded payload (advanced use comment for text)'),
202
+ stateInit: z.string().optional().describe('Optional stateInit BOC for contract deployment'),
203
+ }, { title: 'Request Transfer', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async ({ to, amount, comment, payload, stateInit }) => {
174
204
  if (!TOKEN) {
175
205
  return {
176
- content: [{ type: 'text', text: 'No token configured. Use request_auth first to authenticate.' }],
206
+ content: [{ type: 'text', text: 'No token configured. Use auth.request first to authenticate.' }],
177
207
  isError: true,
178
208
  };
179
209
  }
180
210
  try {
181
- const body = { to, amountNano };
211
+ const body = { to, amount };
212
+ if (comment)
213
+ body.comment = comment;
182
214
  if (payload)
183
215
  body.payload = payload;
184
216
  if (stateInit)
@@ -194,13 +226,13 @@ server.tool('request_transfer', 'Request a TON transfer. The user must approve i
194
226
  text: [
195
227
  `Transfer request created.`,
196
228
  `ID: ${result.id}`,
197
- `To: ${result.to}`,
198
- `Amount: ${result.amountNano} nanoTON`,
229
+ `To: ${result.resolved ? `${result.resolved.from} → ${result.resolved.to}` : result.to}`,
230
+ `Amount: ${amount} TON (${result.amountNano} nanoTON)`,
199
231
  `Status: ${result.status}`,
200
- `Expires: ${new Date(result.expiresAt).toISOString()}`,
232
+ comment ? `Comment: ${comment}` : null,
201
233
  ``,
202
- `The wallet owner must approve this in their TON Connect client.`,
203
- ].join('\n'),
234
+ `Approve in your wallet app.`,
235
+ ].filter(Boolean).join('\n'),
204
236
  },
205
237
  ],
206
238
  };
@@ -212,12 +244,12 @@ server.tool('request_transfer', 'Request a TON transfer. The user must approve i
212
244
  };
213
245
  }
214
246
  });
215
- server.tool('get_request_status', 'Check the status of a transfer request. Statuses: pending (waiting for approval), confirmed (signed and broadcast), rejected (user declined), expired (5 min timeout). Also shows broadcast result if available.', {
216
- id: z.string().describe('The request ID returned by request_transfer'),
217
- }, async ({ id }) => {
247
+ server.tool('transfer.status', 'Check the status of a transfer request. Statuses: pending (waiting for approval), confirmed (signed and broadcast), rejected (user declined), expired (5 min timeout). Also shows broadcast result if available.', {
248
+ id: z.string().describe('The request ID returned by transfer.request'),
249
+ }, { title: 'Transfer Status', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ id }) => {
218
250
  if (!TOKEN) {
219
251
  return {
220
- content: [{ type: 'text', text: 'No token configured. Use request_auth first to authenticate.' }],
252
+ content: [{ type: 'text', text: 'No token configured. Use auth.request first to authenticate.' }],
221
253
  isError: true,
222
254
  };
223
255
  }
@@ -251,10 +283,10 @@ server.tool('get_request_status', 'Check the status of a transfer request. Statu
251
283
  };
252
284
  }
253
285
  });
254
- server.tool('list_pending_requests', 'List all transfer requests waiting for wallet owner approval. Use to check if there are unfinished transfers.', {}, async () => {
286
+ server.tool('transfer.pending', 'List all transfer requests waiting for wallet owner approval. Use to check if there are unfinished transfers.', {}, { title: 'Pending Transfers', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async () => {
255
287
  if (!TOKEN) {
256
288
  return {
257
- content: [{ type: 'text', text: 'No token configured. Use request_auth first to authenticate.' }],
289
+ content: [{ type: 'text', text: 'No token configured. Use auth.request first to authenticate.' }],
258
290
  isError: true,
259
291
  };
260
292
  }
@@ -283,9 +315,59 @@ server.tool('list_pending_requests', 'List all transfer requests waiting for wal
283
315
  };
284
316
  }
285
317
  });
286
- server.tool('get_wallet_info', 'Get the connected wallet address, TON balance (in nanoTON and TON), and account status. Use this to check how much TON the user has before sending transfers.', {}, async () => {
318
+ server.tool('transfer.batch', 'Send multiple TON transfers in a SINGLE wallet approval. Up to 4 transfers per batch (v4 wallet). All transfers appear as one transaction to sign. Use for batch payments, multi-recipient sends, or multiple DEX orders at once.', {
319
+ transfers: z.string().describe('JSON array of transfers: [{"to":"addr","amountNano":"1000000000","comment":"optional text"},...]'),
320
+ }, { title: 'Batch Transfer', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async ({ transfers: transfersJson }) => {
321
+ if (!TOKEN) {
322
+ return { content: [{ type: 'text', text: 'No token configured. Use auth.request first.' }], isError: true };
323
+ }
324
+ try {
325
+ const transfers = JSON.parse(transfersJson);
326
+ if (!transfers.length)
327
+ throw new Error('No transfers provided');
328
+ if (transfers.length > 4)
329
+ throw new Error('Max 4 transfers per batch (v4 wallet). Use agent_wallet.batch_transfer for more.');
330
+ // Encode comments as payloads
331
+ const processed = [];
332
+ for (const t of transfers) {
333
+ const entry = { to: t.to, amountNano: t.amountNano };
334
+ if (t.comment) {
335
+ const { beginCell } = await import('@ton/core');
336
+ const commentCell = beginCell().storeUint(0, 32).storeStringTail(t.comment).endCell();
337
+ entry.payload = commentCell.toBoc().toString('base64');
338
+ }
339
+ processed.push(entry);
340
+ }
341
+ const result = await apiCall('/v1/safe/tx/batch', {
342
+ method: 'POST',
343
+ body: JSON.stringify({ transfers: processed }),
344
+ });
345
+ const totalNano = BigInt(result.batch?.totalNano ?? '0');
346
+ const totalTon = (totalNano / 1000000000n).toString() + '.' +
347
+ (totalNano % 1000000000n).toString().padStart(9, '0').replace(/0+$/, '') || '0';
348
+ return {
349
+ content: [{
350
+ type: 'text',
351
+ text: [
352
+ `Batch transfer created! ${transfers.length} transfers in 1 approval.`,
353
+ '',
354
+ `Total: ${totalTon} TON`,
355
+ `Request ID: ${result.id}`,
356
+ '',
357
+ ...transfers.map((t, i) => ` ${i + 1}. ${t.amountNano} nanoTON → ${t.to.slice(0, 16)}...${t.comment ? ` "${t.comment}"` : ''}`),
358
+ '',
359
+ 'Approve in your wallet app — one signature for all transfers.',
360
+ ].join('\n'),
361
+ }],
362
+ };
363
+ }
364
+ catch (e) {
365
+ return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
366
+ }
367
+ });
368
+ server.tool('wallet.info', 'Get the connected wallet address, TON balance (in nanoTON and TON), and account status. Use this to check how much TON the user has before sending transfers.', {}, { title: 'Wallet Info', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async () => {
287
369
  if (!TOKEN) {
288
- return { content: [{ type: 'text', text: 'No token configured. Use request_auth first.' }], isError: true };
370
+ return { content: [{ type: 'text', text: 'No token configured. Use auth.request first.' }], isError: true };
289
371
  }
290
372
  try {
291
373
  const result = await apiCall('/v1/wallet/balance');
@@ -306,9 +388,9 @@ server.tool('get_wallet_info', 'Get the connected wallet address, TON balance (i
306
388
  return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
307
389
  }
308
390
  });
309
- server.tool('get_jetton_balances', 'List all tokens (jettons) in the wallet — USDT, NOT, DOGS, and others. Shows symbol, name, balance, and decimals for each. Use this when the user asks about their tokens or wants to know what they hold.', {}, async () => {
391
+ server.tool('wallet.jettons', 'List all tokens (jettons) in the wallet — USDT, NOT, DOGS, and others. Shows symbol, name, balance, and decimals for each. Use this when the user asks about their tokens or wants to know what they hold.', {}, { title: 'Jetton Balances', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async () => {
310
392
  if (!TOKEN) {
311
- return { content: [{ type: 'text', text: 'No token configured. Use request_auth first.' }], isError: true };
393
+ return { content: [{ type: 'text', text: 'No token configured. Use auth.request first.' }], isError: true };
312
394
  }
313
395
  try {
314
396
  const result = await apiCall('/v1/wallet/jettons');
@@ -331,11 +413,11 @@ server.tool('get_jetton_balances', 'List all tokens (jettons) in the wallet —
331
413
  return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
332
414
  }
333
415
  });
334
- server.tool('get_transactions', 'Get recent transaction history. Shows timestamps, action types, and whether transactions were flagged as scam. Use when the user asks "what happened" or wants to review recent activity.', {
416
+ server.tool('wallet.transactions', 'Get recent transaction history. Shows timestamps, action types, and whether transactions were flagged as scam. Use when the user asks "what happened" or wants to review recent activity.', {
335
417
  limit: z.number().optional().describe('Number of transactions to return (default 10)'),
336
- }, async ({ limit }) => {
418
+ }, { title: 'Transaction History', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ limit }) => {
337
419
  if (!TOKEN) {
338
- return { content: [{ type: 'text', text: 'No token configured. Use request_auth first.' }], isError: true };
420
+ return { content: [{ type: 'text', text: 'No token configured. Use auth.request first.' }], isError: true };
339
421
  }
340
422
  try {
341
423
  const result = await apiCall(`/v1/wallet/transactions?limit=${limit ?? 10}`);
@@ -356,9 +438,9 @@ server.tool('get_transactions', 'Get recent transaction history. Shows timestamp
356
438
  return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
357
439
  }
358
440
  });
359
- server.tool('get_nft_items', 'List all NFTs owned by the wallet — name, collection, and address for each. Use when the user asks about their NFTs or collectibles.', {}, async () => {
441
+ server.tool('wallet.nfts', 'List all NFTs owned by the wallet — name, collection, and address for each. Use when the user asks about their NFTs or collectibles.', {}, { title: 'NFT Items', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async () => {
360
442
  if (!TOKEN) {
361
- return { content: [{ type: 'text', text: 'No token configured. Use request_auth first.' }], isError: true };
443
+ return { content: [{ type: 'text', text: 'No token configured. Use auth.request first.' }], isError: true };
362
444
  }
363
445
  try {
364
446
  const result = await apiCall('/v1/wallet/nfts');
@@ -375,9 +457,9 @@ server.tool('get_nft_items', 'List all NFTs owned by the wallet — name, collec
375
457
  return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
376
458
  }
377
459
  });
378
- server.tool('resolve_name', 'Resolve a .ton domain name (like "alice.ton") to a raw wallet address. ALWAYS use this before request_transfer when the user gives a .ton name instead of a raw address.', {
460
+ server.tool('lookup.resolve_name', 'Resolve a .ton domain name (like "alice.ton") to a raw wallet address. ALWAYS use this before transfer.request when the user gives a .ton name instead of a raw address.', {
379
461
  domain: z.string().describe('The .ton domain name to resolve (e.g. "alice.ton")'),
380
- }, async ({ domain }) => {
462
+ }, { title: 'Resolve .ton Name', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ domain }) => {
381
463
  try {
382
464
  const result = await fetch(`${API_URL}/v1/dns/${encodeURIComponent(domain)}/resolve`);
383
465
  const data = await result.json();
@@ -394,9 +476,9 @@ server.tool('resolve_name', 'Resolve a .ton domain name (like "alice.ton") to a
394
476
  return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
395
477
  }
396
478
  });
397
- server.tool('get_ton_price', 'Get the current TON price in USD, EUR, or other currencies. Use when the user asks "how much is my TON worth" or before transfers to show USD equivalents.', {
479
+ server.tool('lookup.price', 'Get the current TON price in USD, EUR, or other currencies. Use when the user asks "how much is my TON worth" or before transfers to show USD equivalents.', {
398
480
  currencies: z.string().optional().describe('Comma-separated currencies (default "USD")'),
399
- }, async ({ currencies }) => {
481
+ }, { title: 'TON Price', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ currencies }) => {
400
482
  try {
401
483
  const curr = currencies || 'USD';
402
484
  const result = await fetch(`${API_URL}/v1/market/price?tokens=TON&currencies=${curr}`);
@@ -413,14 +495,14 @@ server.tool('get_ton_price', 'Get the current TON price in USD, EUR, or other cu
413
495
  return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
414
496
  }
415
497
  });
416
- server.tool('create_dex_order', 'Place a limit order on the open4dev DEX order book. Provide the token pair, amount in smallest units, and price in human-readable format (e.g. price=20 means "1 fromToken = 20 toToken"). The API handles all decimal conversions and slippage automatically. The order requires wallet approval.', {
498
+ server.tool('dex.create_order', 'Place a limit order on the open4dev DEX order book. Both amount and price are human-readable the API converts to raw units automatically. The order requires wallet approval. Slippage (4% including fees) is applied automatically.', {
417
499
  fromToken: z.string().describe('Token to sell, e.g. "NOT", "TON", "USDT"'),
418
500
  toToken: z.string().describe('Token to buy, e.g. "TON", "NOT", "AGNT"'),
419
- amount: z.string().describe('Amount to sell in smallest unit. TON/NOT/DOGS/BUILD/AGNT use 9 decimals (1 TON = 10^9). USDT/XAUT0 use 6 decimals (1 USDT = 10^6).'),
420
- price: z.number().describe('Human-readable price: how many toToken per 1 fromToken. E.g. price=20 means "1 USDT = 20 AGNT". price=0.05 means "1 AGNT = 0.05 USDT".'),
421
- }, async ({ fromToken, toToken, amount, price }) => {
501
+ amount: z.string().describe('Human-readable amount to sell, e.g. "10000" for 10,000 NOT or "5" for 5 USDT'),
502
+ price: z.number().describe('Human-readable price: how many toToken per 1 fromToken. E.g. price=20 means "1 USDT = 20 AGNT". price=0.000289 means "1 NOT = 0.000289 TON".'),
503
+ }, { title: 'Create DEX Order', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async ({ fromToken, toToken, amount, price }) => {
422
504
  if (!TOKEN) {
423
- return { content: [{ type: 'text', text: 'No token configured. Use request_auth first.' }], isError: true };
505
+ return { content: [{ type: 'text', text: 'No token configured. Use auth.request first.' }], isError: true };
424
506
  }
425
507
  try {
426
508
  const result = await apiCall('/v1/dex/order', {
@@ -448,7 +530,7 @@ server.tool('create_dex_order', 'Place a limit order on the open4dev DEX order b
448
530
  return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
449
531
  }
450
532
  });
451
- server.tool('list_dex_pairs', 'List available trading pairs on the DEX. Shows which token swaps are configured and available.', {}, async () => {
533
+ server.tool('dex.pairs', 'List available trading pairs on the DEX. Shows which token swaps are configured and available.', {}, { title: 'DEX Pairs', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async () => {
452
534
  try {
453
535
  const result = await fetch(`${API_URL}/v1/dex/pairs`);
454
536
  const data = await result.json();
@@ -464,9 +546,9 @@ server.tool('list_dex_pairs', 'List available trading pairs on the DEX. Shows wh
464
546
  return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
465
547
  }
466
548
  });
467
- server.tool('deploy_agent_wallet', 'Deploy an Agent Wallet smart contract — a dedicated sub-wallet for autonomous operations. WARNING: The agent can spend funds from this wallet WITHOUT user approval. Only deploy if the user explicitly wants autonomous transfers. After deployment, top up the wallet with funds.', {}, async () => {
549
+ server.tool('agent_wallet.deploy', 'Deploy an Agent Wallet smart contract — a dedicated sub-wallet for autonomous operations. WARNING: The agent can spend funds from this wallet WITHOUT user approval. Only deploy if the user explicitly wants autonomous transfers. After deployment, top up the wallet with funds.', {}, { title: 'Deploy Agent Wallet', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async () => {
468
550
  if (!TOKEN) {
469
- return { content: [{ type: 'text', text: 'No token configured. Use request_auth first.' }], isError: true };
551
+ return { content: [{ type: 'text', text: 'No token configured. Use auth.request first.' }], isError: true };
470
552
  }
471
553
  try {
472
554
  // Get owner's public key
@@ -547,13 +629,13 @@ server.tool('deploy_agent_wallet', 'Deploy an Agent Wallet smart contract — a
547
629
  return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
548
630
  }
549
631
  });
550
- 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. Use for automated/autonomous transfers where speed matters and the user has opted in.', {
632
+ server.tool('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. Use for automated/autonomous transfers where speed matters and the user has opted in.', {
551
633
  walletAddress: z.string().describe('The agent wallet contract address'),
552
634
  to: z.string().describe('Destination TON address'),
553
635
  amountNano: z.string().describe('Amount in nanoTON'),
554
- }, async ({ walletAddress, to, amountNano }) => {
636
+ }, { title: 'Agent Wallet Transfer', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }, async ({ walletAddress, to, amountNano }) => {
555
637
  if (!TOKEN) {
556
- return { content: [{ type: 'text', text: 'No token configured. Use request_auth first.' }], isError: true };
638
+ return { content: [{ type: 'text', text: 'No token configured. Use auth.request first.' }], isError: true };
557
639
  }
558
640
  try {
559
641
  const { Cell, beginCell, Address, SendMode, external, storeMessage } = await import('@ton/core');
@@ -644,11 +726,112 @@ server.tool('execute_agent_wallet_transfer', 'Send TON directly from an Agent Wa
644
726
  return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
645
727
  }
646
728
  });
647
- server.tool('get_agent_wallet_info', 'Get info about Agent Walletsbalance, seqno, agent key status. Pass a wallet address for details, or omit to list all agent wallets.', {
729
+ server.tool('agent_wallet.batch_transfer', 'Send multiple TON transfers in a SINGLE transaction from an Agent WalletNO approval needed. All transfers are executed atomically in one external message. Max 255 actions per transaction.', {
730
+ walletAddress: z.string().describe('The agent wallet contract address'),
731
+ transfers: z.string().describe('JSON array of transfers: [{"to":"addr","amountNano":"1000000000"},...]'),
732
+ }, { title: 'Batch Transfer from Agent Wallet', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }, async ({ walletAddress, transfers: transfersJson }) => {
733
+ if (!TOKEN) {
734
+ return { content: [{ type: 'text', text: 'No token configured. Use auth.request first.' }], isError: true };
735
+ }
736
+ try {
737
+ const { Cell, beginCell, Address, SendMode, external, storeMessage } = await import('@ton/core');
738
+ const { sign, keyPairFromSeed } = await import('@ton/crypto');
739
+ const transfers = JSON.parse(transfersJson);
740
+ if (!transfers.length)
741
+ throw new Error('No transfers provided');
742
+ if (transfers.length > 255)
743
+ throw new Error('Max 255 transfers per batch');
744
+ const localWallets = loadLocalWallets();
745
+ const localConfig = localWallets[walletAddress];
746
+ if (!localConfig)
747
+ throw new Error('Agent wallet secret key not found locally.');
748
+ const infoResult = await apiCall(`/v1/agent-wallet/${encodeURIComponent(walletAddress)}/info`);
749
+ const seqno = infoResult.seqno;
750
+ const walletId = localConfig.walletId;
751
+ const secretKeyBuf = Buffer.from(localConfig.agentSecretKey, 'hex');
752
+ let secretKey;
753
+ if (secretKeyBuf.length === 64) {
754
+ secretKey = secretKeyBuf;
755
+ }
756
+ else {
757
+ const kp = keyPairFromSeed(secretKeyBuf);
758
+ secretKey = kp.secretKey;
759
+ }
760
+ // Build actions list with all transfers
761
+ const sendMode = SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS;
762
+ let actionsList = beginCell().endCell(); // empty tail
763
+ for (const t of transfers) {
764
+ const destAddr = Address.parse(t.to);
765
+ const transferMsg = beginCell()
766
+ .storeUint(0x18, 6)
767
+ .storeAddress(destAddr)
768
+ .storeCoins(BigInt(t.amountNano))
769
+ .storeUint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
770
+ .endCell();
771
+ actionsList = beginCell()
772
+ .storeRef(actionsList)
773
+ .storeUint(0x0ec3c86d, 32)
774
+ .storeUint(sendMode, 8)
775
+ .storeRef(transferMsg)
776
+ .endCell();
777
+ }
778
+ // Build and sign external message
779
+ const validUntil = Math.floor(Date.now() / 1000) + 300;
780
+ const unsignedBody = beginCell()
781
+ .storeUint(0x7369676e, 32)
782
+ .storeUint(walletId, 32)
783
+ .storeUint(validUntil, 32)
784
+ .storeUint(seqno, 32)
785
+ .storeMaybeRef(actionsList)
786
+ .endCell();
787
+ const signature = sign(unsignedBody.hash(), secretKey);
788
+ const signedBody = beginCell()
789
+ .storeSlice(unsignedBody.beginParse())
790
+ .storeBuffer(signature)
791
+ .endCell();
792
+ const vaultAddr = Address.parse(walletAddress);
793
+ const extMsg = external({ to: vaultAddr, body: signedBody });
794
+ const boc = beginCell().store(storeMessage(extMsg)).endCell().toBoc().toString('base64');
795
+ // Broadcast
796
+ const broadcastRes = await fetch('https://toncenter.com/api/v2/sendBoc', {
797
+ method: 'POST',
798
+ headers: { 'Content-Type': 'application/json' },
799
+ body: JSON.stringify({ boc }),
800
+ });
801
+ const broadcastData = await broadcastRes.json();
802
+ if (!broadcastData.ok) {
803
+ throw new Error(`Broadcast failed: ${broadcastData.error || JSON.stringify(broadcastData)}`);
804
+ }
805
+ const totalNano = transfers.reduce((sum, t) => sum + BigInt(t.amountNano), 0n);
806
+ const totalTon = (totalNano / 1000000000n).toString() + '.' +
807
+ (totalNano % 1000000000n).toString().padStart(9, '0').replace(/0+$/, '');
808
+ return {
809
+ content: [{
810
+ type: 'text',
811
+ text: [
812
+ `Batch transfer executed! ${transfers.length} transfers in 1 transaction.`,
813
+ '',
814
+ `From: ${walletAddress}`,
815
+ `Total: ${totalTon} TON`,
816
+ `Transfers: ${transfers.length}`,
817
+ `Seqno: ${seqno}`,
818
+ '',
819
+ ...transfers.map((t, i) => ` ${i + 1}. ${t.amountNano} nanoTON → ${t.to.slice(0, 12)}...`),
820
+ '',
821
+ 'All broadcast successfully. No approval needed.',
822
+ ].join('\n'),
823
+ }],
824
+ };
825
+ }
826
+ catch (e) {
827
+ return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
828
+ }
829
+ });
830
+ server.tool('agent_wallet.info', 'Get info about Agent Wallets — balance, seqno, agent key status. Pass a wallet address for details, or omit to list all agent wallets.', {
648
831
  walletAddress: z.string().optional().describe('Agent wallet address. If omitted, lists all your agent wallets.'),
649
- }, async ({ walletAddress }) => {
832
+ }, { title: 'Agent Wallet Info', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ walletAddress }) => {
650
833
  if (!TOKEN) {
651
- return { content: [{ type: 'text', text: 'No token configured. Use request_auth first.' }], isError: true };
834
+ return { content: [{ type: 'text', text: 'No token configured. Use auth.request first.' }], isError: true };
652
835
  }
653
836
  try {
654
837
  if (!walletAddress) {
@@ -656,7 +839,7 @@ server.tool('get_agent_wallet_info', 'Get info about Agent Wallets — balance,
656
839
  const result = await apiCall('/v1/agent-wallet/list');
657
840
  const wallets = result.wallets || [];
658
841
  if (!wallets.length) {
659
- return { content: [{ type: 'text', text: 'No agent wallets found. Use deploy_agent_wallet to create one.' }] };
842
+ return { content: [{ type: 'text', text: 'No agent wallets found. Use agent_wallet.deploy to create one.' }] };
660
843
  }
661
844
  const lines = wallets.map((w) => `- ${w.address} (created ${new Date(w.createdAt).toISOString()})`);
662
845
  return {
@@ -700,7 +883,7 @@ if (httpPort) {
700
883
  // Health check
701
884
  if (req.method === 'GET' && req.url === '/health') {
702
885
  res.writeHead(200, { 'Content-Type': 'application/json' });
703
- res.end(JSON.stringify({ ok: true, version: '0.12.0' }));
886
+ res.end(JSON.stringify({ ok: true, version: '0.14.0' }));
704
887
  return;
705
888
  }
706
889
  // MCP endpoint
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Cloudflare Worker — MCP HTTP transport for Smithery and remote clients.
3
+ * Mirrors all 16 tools from the stdio version with full descriptions and annotations.
4
+ */
5
+ declare const _default: {
6
+ fetch(request: Request): Promise<Response>;
7
+ };
8
+ export default _default;
package/dist/worker.js ADDED
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Cloudflare Worker — MCP HTTP transport for Smithery and remote clients.
3
+ * Mirrors all 16 tools from the stdio version with full descriptions and annotations.
4
+ */
5
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
7
+ import { z } from 'zod';
8
+ const VERSION = '0.14.0';
9
+ function createMcpServer() {
10
+ const server = new McpServer({
11
+ name: 'tongateway',
12
+ version: VERSION,
13
+ });
14
+ // --- Auth ---
15
+ server.tool('auth.request', 'Authenticate with TON blockchain. Generates a one-time link for the user to connect their wallet. After the user opens the link, call auth.get_token to complete authentication.', { label: z.string().optional().describe('Label for this agent session, e.g. "claude-agent"') }, { title: 'Request Authentication', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async () => ({ content: [{ type: 'text', text: 'Use stdio transport (npx @tongateway/mcp) for full tool execution.' }] }));
16
+ server.tool('auth.get_token', 'Complete authentication after the user opened the link from auth.request. Returns the token which enables all other tools.', { authId: z.string().describe('The authId returned by auth.request') }, { title: 'Get Auth Token', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async () => ({ content: [{ type: 'text', text: 'Use stdio transport for full tool execution.' }] }));
17
+ // --- Wallet ---
18
+ server.tool('wallet.info', 'Get the connected wallet address, TON balance in nanoTON and human-readable format, and account status (active/uninitialized).', {}, { title: 'Wallet Info', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async () => ({ content: [{ type: 'text', text: 'Use stdio transport for full tool execution.' }] }));
19
+ server.tool('wallet.jettons', 'List all jetton (token) balances in the wallet — USDT, NOT, DOGS, BUILD, and others. Returns symbol, name, balance, and decimals for each token.', {}, { title: 'Jetton Balances', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async () => ({ content: [{ type: 'text', text: 'Use stdio transport for full tool execution.' }] }));
20
+ server.tool('wallet.transactions', 'Get recent transaction history for the connected wallet. Shows timestamps, action types, and scam flags.', { limit: z.number().optional().describe('Number of transactions to return (default 10, max 100)') }, { title: 'Transaction History', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async () => ({ content: [{ type: 'text', text: 'Use stdio transport for full tool execution.' }] }));
21
+ server.tool('wallet.nfts', 'List all NFTs owned by the connected wallet — name, collection name, and contract address for each.', {}, { title: 'NFT Items', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async () => ({ content: [{ type: 'text', text: 'Use stdio transport for full tool execution.' }] }));
22
+ // --- Transfers ---
23
+ server.tool('transfer.request', 'Request a TON transfer that the wallet owner must approve. Amount is in nanoTON (1 TON = 1000000000). Supports optional payload BOC and stateInit for contract deployment. Use transfer.status to check approval.', {
24
+ to: z.string().describe('Destination TON address (raw format 0:abc... or friendly EQ...)'),
25
+ amountNano: z.string().describe('Amount in nanoTON. 1 TON = 1000000000 nanoTON'),
26
+ payload: z.string().optional().describe('Optional BOC-encoded payload for the transaction'),
27
+ stateInit: z.string().optional().describe('Optional stateInit BOC for deploying new smart contracts'),
28
+ }, { title: 'Request Transfer', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async () => ({ content: [{ type: 'text', text: 'Use stdio transport for full tool execution.' }] }));
29
+ server.tool('transfer.status', 'Check the status of a transfer request. Returns: pending (waiting for approval), confirmed (signed and broadcast), rejected (user declined), or expired (5 min timeout). Also shows broadcast result if available.', { id: z.string().describe('The request ID returned by transfer.request') }, { title: 'Transfer Status', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async () => ({ content: [{ type: 'text', text: 'Use stdio transport for full tool execution.' }] }));
30
+ server.tool('transfer.pending', 'List all transfer requests currently waiting for wallet owner approval.', {}, { title: 'Pending Transfers', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async () => ({ content: [{ type: 'text', text: 'Use stdio transport for full tool execution.' }] }));
31
+ // --- Lookup ---
32
+ server.tool('lookup.resolve_name', 'Resolve a .ton domain name (e.g. "alice.ton") to a raw wallet address. Always use this before transfer.request when the user provides a .ton name.', { domain: z.string().describe('The .ton domain name to resolve, e.g. "alice.ton" or "foundation.ton"') }, { title: 'Resolve .ton Name', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async () => ({ content: [{ type: 'text', text: 'Use stdio transport for full tool execution.' }] }));
33
+ server.tool('lookup.price', 'Get the current price of TON in USD, EUR, or other fiat currencies. Use to show users the value of their holdings.', { currencies: z.string().optional().describe('Comma-separated currency codes, e.g. "USD,EUR". Default: "USD"') }, { title: 'TON Price', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async () => ({ content: [{ type: 'text', text: 'Use stdio transport for full tool execution.' }] }));
34
+ // --- DEX ---
35
+ server.tool('dex.create_order', 'Place a limit order on the open4dev DEX order book. Both amount and price are human-readable. The API converts to raw units, handles slippage (4% including fees), and gas automatically.', {
36
+ fromToken: z.string().describe('Token to sell: TON, NOT, USDT, DOGS, BUILD, AGNT, CBBTC, PX, XAUT0'),
37
+ toToken: z.string().describe('Token to buy: TON, NOT, USDT, DOGS, BUILD, AGNT, CBBTC, PX, XAUT0'),
38
+ amount: z.string().describe('Human-readable amount to sell, e.g. "10000" for 10,000 NOT or "5" for 5 USDT'),
39
+ price: z.number().describe('Human-readable price: how many toToken per 1 fromToken. E.g. price=20 means "1 USDT = 20 AGNT".'),
40
+ }, { title: 'Create DEX Order', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async () => ({ content: [{ type: 'text', text: 'Use stdio transport for full tool execution.' }] }));
41
+ server.tool('dex.pairs', 'List all available trading pairs and tokens on the open4dev DEX. Returns supported tokens: TON, NOT, USDT, DOGS, BUILD, AGNT, CBBTC, PX, XAUT0.', {}, { title: 'DEX Pairs', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async () => ({ content: [{ type: 'text', text: 'Use stdio transport for full tool execution.' }] }));
42
+ // --- Agent Wallet ---
43
+ server.tool('agent_wallet.deploy', 'Deploy an Agent Wallet smart contract — a dedicated sub-wallet for autonomous transfers without approval. WARNING: The agent can spend all funds in this wallet without user confirmation. Only deploy when the user explicitly requests autonomous mode.', {}, { title: 'Deploy Agent Wallet', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async () => ({ content: [{ type: 'text', text: 'Use stdio transport for full tool execution.' }] }));
44
+ server.tool('agent_wallet.transfer', 'Send TON directly from an Agent Wallet — NO approval needed. Signs and broadcasts the transaction immediately. Only works with deployed agent wallets where the agent key is authorized.', {
45
+ walletAddress: z.string().describe('The agent wallet contract address'),
46
+ to: z.string().describe('Destination TON address'),
47
+ amountNano: z.string().describe('Amount in nanoTON (1 TON = 1000000000)'),
48
+ }, { title: 'Agent Wallet Transfer', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }, async () => ({ content: [{ type: 'text', text: 'Use stdio transport for full tool execution.' }] }));
49
+ server.tool('agent_wallet.info', 'Get info about Agent Wallets — balance, seqno, and agent key status. Pass a wallet address for details, or omit to list all your agent wallets.', { walletAddress: z.string().optional().describe('Agent wallet address. Omit to list all wallets.') }, { title: 'Agent Wallet Info', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async () => ({ content: [{ type: 'text', text: 'Use stdio transport for full tool execution.' }] }));
50
+ // --- Prompts ---
51
+ server.prompt('quickstart', 'How to get started with Agent Gateway on TON blockchain', async () => ({
52
+ messages: [{
53
+ role: 'user',
54
+ content: {
55
+ type: 'text',
56
+ text: `# Quick Start
57
+
58
+ ## Install
59
+ \`\`\`bash
60
+ claude mcp add-json tongateway '{"command":"npx","args":["-y","@tongateway/mcp"],"env":{"AGENT_GATEWAY_API_URL":"https://api.tongateway.ai"}}' --scope user
61
+ \`\`\`
62
+
63
+ ## First use
64
+ The agent authenticates automatically — it generates a link, you open it and connect your wallet once. Token persists across restarts.
65
+
66
+ ## Try these commands:
67
+ - "What's my TON balance?"
68
+ - "Show my tokens"
69
+ - "Send 1 TON to alice.ton"
70
+ - "What's the current TON price?"
71
+ - "Show my NFTs"`,
72
+ },
73
+ }],
74
+ }));
75
+ server.prompt('token-reference', 'Amount conversion and token decimals reference', async () => ({
76
+ messages: [{
77
+ role: 'user',
78
+ content: {
79
+ type: 'text',
80
+ text: `# Token Reference
81
+
82
+ ## Amount conversion (nanoTON)
83
+ | TON | nanoTON |
84
+ |-------|----------------|
85
+ | 0.1 | 100000000 |
86
+ | 0.5 | 500000000 |
87
+ | 1 | 1000000000 |
88
+ | 10 | 10000000000 |
89
+
90
+ ## Token decimals
91
+ - 9 decimals: TON, NOT, DOGS, BUILD, AGNT, PX, CBBTC
92
+ - 6 decimals: USDT, XAUT0
93
+
94
+ ## DEX price format
95
+ Price is human-readable: price=20 means "1 fromToken = 20 toToken"
96
+ Example: USDT→AGNT at price=20 means "1 USDT = 20 AGNT"`,
97
+ },
98
+ }],
99
+ }));
100
+ server.prompt('example-transfer', 'Example: Send TON to a .ton domain with price check', async () => ({
101
+ messages: [{
102
+ role: 'user',
103
+ content: {
104
+ type: 'text',
105
+ text: `# Example: Send TON to alice.ton
106
+
107
+ ## Step 1: Check balance
108
+ wallet.info()
109
+ → Address: 0:9d43...0c02, Balance: 823.18 TON, Status: active
110
+
111
+ ## Step 2: Check price
112
+ lookup.price({ currencies: "USD" })
113
+ → 1 TON = $2.45 USD
114
+
115
+ ## Step 3: Resolve .ton domain
116
+ lookup.resolve_name({ domain: "alice.ton" })
117
+ → alice.ton → 0:83df...31a8
118
+
119
+ ## Step 4: Send transfer
120
+ transfer.request({ to: "0:83df...31a8", amountNano: "500000000" })
121
+ → Transfer request created (ID: abc-123). Approve in your wallet app.
122
+
123
+ ## Step 5: Check status
124
+ transfer.status({ id: "abc-123" })
125
+ → Status: confirmed, Broadcast: success`,
126
+ },
127
+ }],
128
+ }));
129
+ server.prompt('example-dex-order', 'Example: Place a DEX order to swap tokens', async () => ({
130
+ messages: [{
131
+ role: 'user',
132
+ content: {
133
+ type: 'text',
134
+ text: `# Example: Swap 10,000 NOT for TON
135
+
136
+ ## Step 1: Check available tokens
137
+ dex.pairs()
138
+ → Available tokens: TON, NOT, USDT, DOGS, BUILD, AGNT, CBBTC, PX, XAUT0
139
+
140
+ ## Step 2: Check your NOT balance
141
+ wallet.jettons()
142
+ → NOT: 3,186,370.60, USDT: 107.79, BUILD: 45,277.57
143
+
144
+ ## Step 3: Get current price
145
+ lookup.price({ currencies: "USD" })
146
+ → 1 TON = $2.45 USD
147
+ (NOT ≈ 0.000289 TON per NOT)
148
+
149
+ ## Step 4: Place order
150
+ dex.create_order({
151
+ fromToken: "NOT",
152
+ toToken: "TON",
153
+ amount: "10000", // human-readable, API converts to raw
154
+ price: 0.000289 // TON per NOT
155
+ })
156
+ → Order placed! Approve in your wallet app.`,
157
+ },
158
+ }],
159
+ }));
160
+ server.prompt('example-agent-wallet', 'Example: Deploy and use an autonomous Agent Wallet', async () => ({
161
+ messages: [{
162
+ role: 'user',
163
+ content: {
164
+ type: 'text',
165
+ text: `# Example: Autonomous Agent Wallet
166
+
167
+ ⚠️ WARNING: Agent Wallet allows spending WITHOUT approval!
168
+
169
+ ## Step 1: Deploy
170
+ agent_wallet.deploy()
171
+ → Agent Wallet deployed at EQCT1... Approve 0.1 TON deploy fee in wallet.
172
+
173
+ ## Step 2: Top up
174
+ transfer.request({ to: "EQCT1...", amountNano: "1000000000" })
175
+ → Transfer 1 TON to agent wallet. Approve in wallet.
176
+
177
+ ## Step 3: Send from agent wallet (NO approval needed!)
178
+ agent_wallet.transfer({
179
+ walletAddress: "0:93d4...",
180
+ to: "0:abc...",
181
+ amountNano: "500000000"
182
+ })
183
+ → Transfer executed. No approval needed.
184
+
185
+ ## Check balance
186
+ agent_wallet.info({ walletAddress: "0:93d4..." })
187
+ → Balance: 0.5 TON, Seqno: 1, Status: active`,
188
+ },
189
+ }],
190
+ }));
191
+ // --- Resources ---
192
+ server.resource('docs', 'https://tongateway.ai/docs', async (uri) => ({
193
+ contents: [{
194
+ uri: uri.href,
195
+ mimeType: 'text/plain',
196
+ text: 'Agent Gateway documentation: https://tongateway.ai/docs\nAPI Swagger: https://api.tongateway.ai/docs\nSkill file: https://tongateway.ai/agent-gateway.md',
197
+ }],
198
+ }));
199
+ server.resource('skill', 'https://tongateway.ai/agent-gateway.md', async (uri) => ({
200
+ contents: [{
201
+ uri: uri.href,
202
+ mimeType: 'text/plain',
203
+ text: 'Skill file with all 16 tool descriptions, usage examples, and amount conversion: https://tongateway.ai/agent-gateway.md',
204
+ }],
205
+ }));
206
+ return server;
207
+ }
208
+ const CORS_HEADERS = {
209
+ 'Access-Control-Allow-Origin': '*',
210
+ 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
211
+ 'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization, Mcp-Session-Id',
212
+ 'Access-Control-Expose-Headers': 'Mcp-Session-Id',
213
+ };
214
+ export default {
215
+ async fetch(request) {
216
+ const url = new URL(request.url);
217
+ if (request.method === 'OPTIONS') {
218
+ return new Response(null, { status: 204, headers: CORS_HEADERS });
219
+ }
220
+ if (request.method === 'GET' && (url.pathname === '/' || url.pathname === '/health')) {
221
+ return new Response(JSON.stringify({
222
+ ok: true,
223
+ name: '@tongateway/mcp',
224
+ version: VERSION,
225
+ description: 'TON blockchain gateway for AI agents — 16 tools for wallet info, transfers, jettons, NFTs, DNS, prices, DEX orders, and agent wallets.',
226
+ tools: 16,
227
+ prompts: 2,
228
+ resources: 2,
229
+ transport: 'streamable-http',
230
+ endpoint: '/mcp',
231
+ install: 'npx -y @tongateway/mcp',
232
+ homepage: 'https://tongateway.ai',
233
+ docs: 'https://tongateway.ai/docs',
234
+ repository: 'https://github.com/tongateway/mcp',
235
+ }), { headers: { 'Content-Type': 'application/json', ...CORS_HEADERS } });
236
+ }
237
+ if (url.pathname === '/mcp') {
238
+ try {
239
+ const server = createMcpServer();
240
+ const transport = new WebStandardStreamableHTTPServerTransport({
241
+ sessionIdGenerator: undefined,
242
+ });
243
+ await server.connect(transport);
244
+ const response = await transport.handleRequest(request);
245
+ const newHeaders = new Headers(response.headers);
246
+ for (const [k, v] of Object.entries(CORS_HEADERS)) {
247
+ newHeaders.set(k, v);
248
+ }
249
+ return new Response(response.body, { status: response.status, headers: newHeaders });
250
+ }
251
+ catch (e) {
252
+ return new Response(JSON.stringify({ error: e.message }), {
253
+ status: 500,
254
+ headers: { 'Content-Type': 'application/json', ...CORS_HEADERS },
255
+ });
256
+ }
257
+ }
258
+ return new Response('Not found', { status: 404, headers: CORS_HEADERS });
259
+ },
260
+ };
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@tongateway/mcp",
3
- "version": "0.13.0",
4
- "description": "MCP server for Agent Gatewaylets AI agents request TON blockchain transfers",
3
+ "version": "0.19.2",
4
+ "description": "TON blockchain gateway for AI agents16 MCP tools: wallet info, transfers, jettons, NFTs, DNS, prices, DEX orders, agent wallets",
5
+ "homepage": "https://tongateway.ai",
5
6
  "license": "MIT",
6
7
  "repository": {
7
8
  "type": "git",
@@ -24,7 +25,8 @@
24
25
  ],
25
26
  "scripts": {
26
27
  "build": "tsc",
27
- "dev": "tsx src/index.ts"
28
+ "dev": "tsx src/index.ts",
29
+ "test": "vitest run"
28
30
  },
29
31
  "dependencies": {
30
32
  "@modelcontextprotocol/sdk": "^1.12.1",
@@ -33,8 +35,11 @@
33
35
  "zod": "^4.3.6"
34
36
  },
35
37
  "devDependencies": {
38
+ "@cloudflare/workers-types": "^4.20260317.1",
36
39
  "@types/node": "^22.17.2",
37
40
  "tsx": "^4.20.5",
38
- "typescript": "^5.9.2"
41
+ "typescript": "^5.9.2",
42
+ "vitest": "^4.1.1",
43
+ "wrangler": "^4.77.0"
39
44
  }
40
45
  }