@tongateway/mcp 0.1.0 → 0.3.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/README.md +43 -0
- package/dist/index.js +243 -7
- package/package.json +14 -5
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# @tongateway/mcp
|
|
2
|
+
|
|
3
|
+
MCP server for [Agent Gateway](https://github.com/pewpewgogo/ton-agent-gateway) — lets AI agents request TON blockchain transfers via Model Context Protocol.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @tongateway/mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Configure
|
|
12
|
+
|
|
13
|
+
Add to your MCP client config (Claude Code, Cursor, etc.):
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"mcpServers": {
|
|
18
|
+
"tongateway": {
|
|
19
|
+
"command": "tongateway-mcp",
|
|
20
|
+
"env": {
|
|
21
|
+
"AGENT_GATEWAY_TOKEN": "YOUR_TOKEN_HERE",
|
|
22
|
+
"AGENT_GATEWAY_API_URL": "https://api.tongateway.ai"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Get your token at [tongateway.ai/app.html](https://tongateway.ai/app.html).
|
|
30
|
+
|
|
31
|
+
## Tools
|
|
32
|
+
|
|
33
|
+
| Tool | Description |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `request_transfer` | Request a TON transfer (to, amountNano, payloadBoc?) |
|
|
36
|
+
| `get_request_status` | Check status of a request by ID |
|
|
37
|
+
| `list_pending_requests` | List all pending requests |
|
|
38
|
+
|
|
39
|
+
## Links
|
|
40
|
+
|
|
41
|
+
- [Agent Gateway](https://github.com/pewpewgogo/ton-agent-gateway) — main repo with all links
|
|
42
|
+
- [Dashboard](https://tongateway.ai) — connect wallet & manage tokens
|
|
43
|
+
- [API Docs](https://api.tongateway.ai/docs) — Swagger UI
|
package/dist/index.js
CHANGED
|
@@ -3,17 +3,18 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
const API_URL = process.env.AGENT_GATEWAY_API_URL ?? 'https://api.tongateway.ai';
|
|
6
|
-
|
|
7
|
-
if (!TOKEN) {
|
|
8
|
-
console.error('AGENT_GATEWAY_TOKEN environment variable is required');
|
|
9
|
-
process.exit(1);
|
|
10
|
-
}
|
|
6
|
+
let TOKEN = process.env.AGENT_GATEWAY_TOKEN || '';
|
|
11
7
|
async function apiCall(path, options = {}) {
|
|
8
|
+
const headers = {
|
|
9
|
+
'Content-Type': 'application/json',
|
|
10
|
+
};
|
|
11
|
+
if (TOKEN) {
|
|
12
|
+
headers['Authorization'] = `Bearer ${TOKEN}`;
|
|
13
|
+
}
|
|
12
14
|
const res = await fetch(`${API_URL}${path}`, {
|
|
13
15
|
...options,
|
|
14
16
|
headers: {
|
|
15
|
-
|
|
16
|
-
Authorization: `Bearer ${TOKEN}`,
|
|
17
|
+
...headers,
|
|
17
18
|
...options.headers,
|
|
18
19
|
},
|
|
19
20
|
});
|
|
@@ -27,11 +28,104 @@ const server = new McpServer({
|
|
|
27
28
|
name: 'agent-gateway',
|
|
28
29
|
version: '0.1.0',
|
|
29
30
|
});
|
|
31
|
+
server.tool('request_auth', 'Request wallet authentication. Generates a one-time link for the user to connect their TON wallet. After the user connects, use get_auth_token to retrieve the token. Use this when no token is configured.', {
|
|
32
|
+
label: z.string().optional().describe('Label for this agent session (e.g. "claude-agent")'),
|
|
33
|
+
}, async ({ label }) => {
|
|
34
|
+
try {
|
|
35
|
+
const result = await fetch(`${API_URL}/v1/auth/request`, {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38
|
+
body: JSON.stringify({ label: label || 'agent' }),
|
|
39
|
+
});
|
|
40
|
+
const data = await result.json();
|
|
41
|
+
if (!result.ok)
|
|
42
|
+
throw new Error(data.error ?? 'Failed');
|
|
43
|
+
return {
|
|
44
|
+
content: [
|
|
45
|
+
{
|
|
46
|
+
type: 'text',
|
|
47
|
+
text: [
|
|
48
|
+
`Authentication requested.`,
|
|
49
|
+
``,
|
|
50
|
+
`Ask the user to open this link:`,
|
|
51
|
+
data.authUrl,
|
|
52
|
+
``,
|
|
53
|
+
`Auth ID: ${data.authId}`,
|
|
54
|
+
`Expires: ${new Date(data.expiresAt).toISOString()}`,
|
|
55
|
+
``,
|
|
56
|
+
`After the user connects their wallet, call get_auth_token with this authId to get the token.`,
|
|
57
|
+
].join('\n'),
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
return {
|
|
64
|
+
content: [{ type: 'text', text: `Error: ${e.message}` }],
|
|
65
|
+
isError: true,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
server.tool('get_auth_token', 'Check if the user has completed wallet authentication and retrieve the token. Call this after request_auth once the user has opened the link and connected their wallet.', {
|
|
70
|
+
authId: z.string().describe('The authId returned by request_auth'),
|
|
71
|
+
}, async ({ authId }) => {
|
|
72
|
+
try {
|
|
73
|
+
const result = await fetch(`${API_URL}/v1/auth/check/${authId}`, {
|
|
74
|
+
headers: { 'Content-Type': 'application/json' },
|
|
75
|
+
});
|
|
76
|
+
const data = await result.json();
|
|
77
|
+
if (!result.ok)
|
|
78
|
+
throw new Error(data.error ?? 'Failed');
|
|
79
|
+
if (data.status === 'pending') {
|
|
80
|
+
return {
|
|
81
|
+
content: [
|
|
82
|
+
{
|
|
83
|
+
type: 'text',
|
|
84
|
+
text: [
|
|
85
|
+
`Authentication still pending.`,
|
|
86
|
+
`The user has not connected their wallet yet.`,
|
|
87
|
+
``,
|
|
88
|
+
`Wait a moment and try again.`,
|
|
89
|
+
].join('\n'),
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// Store the token for future API calls
|
|
95
|
+
TOKEN = data.token;
|
|
96
|
+
return {
|
|
97
|
+
content: [
|
|
98
|
+
{
|
|
99
|
+
type: 'text',
|
|
100
|
+
text: [
|
|
101
|
+
`Authentication complete!`,
|
|
102
|
+
`Token received and configured.`,
|
|
103
|
+
`Wallet: ${data.address}`,
|
|
104
|
+
``,
|
|
105
|
+
`You can now use request_transfer, get_request_status, and list_pending_requests.`,
|
|
106
|
+
].join('\n'),
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
return {
|
|
113
|
+
content: [{ type: 'text', text: `Error: ${e.message}` }],
|
|
114
|
+
isError: true,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
});
|
|
30
118
|
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.', {
|
|
31
119
|
to: z.string().describe('Destination TON address'),
|
|
32
120
|
amountNano: z.string().describe('Amount in nanoTON (1 TON = 1000000000)'),
|
|
33
121
|
payloadBoc: z.string().optional().describe('Optional BOC-encoded payload for the transaction'),
|
|
34
122
|
}, async ({ to, amountNano, payloadBoc }) => {
|
|
123
|
+
if (!TOKEN) {
|
|
124
|
+
return {
|
|
125
|
+
content: [{ type: 'text', text: 'No token configured. Use request_auth first to authenticate.' }],
|
|
126
|
+
isError: true,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
35
129
|
try {
|
|
36
130
|
const body = { to, amountNano };
|
|
37
131
|
if (payloadBoc)
|
|
@@ -68,6 +162,12 @@ server.tool('request_transfer', 'Request a TON transfer from the wallet owner. T
|
|
|
68
162
|
server.tool('get_request_status', 'Check the status of a previously submitted transfer request.', {
|
|
69
163
|
id: z.string().describe('The request ID returned by request_transfer'),
|
|
70
164
|
}, async ({ id }) => {
|
|
165
|
+
if (!TOKEN) {
|
|
166
|
+
return {
|
|
167
|
+
content: [{ type: 'text', text: 'No token configured. Use request_auth first to authenticate.' }],
|
|
168
|
+
isError: true,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
71
171
|
try {
|
|
72
172
|
const result = await apiCall(`/v1/safe/tx/${id}`);
|
|
73
173
|
return {
|
|
@@ -97,6 +197,12 @@ server.tool('get_request_status', 'Check the status of a previously submitted tr
|
|
|
97
197
|
}
|
|
98
198
|
});
|
|
99
199
|
server.tool('list_pending_requests', 'List all pending transfer requests waiting for wallet owner approval.', {}, async () => {
|
|
200
|
+
if (!TOKEN) {
|
|
201
|
+
return {
|
|
202
|
+
content: [{ type: 'text', text: 'No token configured. Use request_auth first to authenticate.' }],
|
|
203
|
+
isError: true,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
100
206
|
try {
|
|
101
207
|
const data = await apiCall('/v1/safe/tx/pending');
|
|
102
208
|
const requests = data.requests;
|
|
@@ -122,5 +228,135 @@ server.tool('list_pending_requests', 'List all pending transfer requests waiting
|
|
|
122
228
|
};
|
|
123
229
|
}
|
|
124
230
|
});
|
|
231
|
+
server.tool('get_wallet_info', 'Get the connected wallet address, TON balance, and account status.', {}, async () => {
|
|
232
|
+
if (!TOKEN) {
|
|
233
|
+
return { content: [{ type: 'text', text: 'No token configured. Use request_auth first.' }], isError: true };
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
const result = await apiCall('/v1/wallet/balance');
|
|
237
|
+
const balanceTon = (BigInt(result.balance) / 1000000000n).toString();
|
|
238
|
+
const balanceFrac = (BigInt(result.balance) % 1000000000n).toString().padStart(9, '0').replace(/0+$/, '') || '0';
|
|
239
|
+
return {
|
|
240
|
+
content: [{
|
|
241
|
+
type: 'text',
|
|
242
|
+
text: [
|
|
243
|
+
`Address: ${result.address}`,
|
|
244
|
+
`Balance: ${balanceTon}.${balanceFrac} TON (${result.balance} nanoTON)`,
|
|
245
|
+
`Status: ${result.status}`,
|
|
246
|
+
].join('\n'),
|
|
247
|
+
}],
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
catch (e) {
|
|
251
|
+
return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
server.tool('get_jetton_balances', 'Get all jetton (token) balances in the connected wallet. Shows USDT, NOT, DOGS, and other tokens.', {}, async () => {
|
|
255
|
+
if (!TOKEN) {
|
|
256
|
+
return { content: [{ type: 'text', text: 'No token configured. Use request_auth first.' }], isError: true };
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
const result = await apiCall('/v1/wallet/jettons');
|
|
260
|
+
if (!result.balances?.length) {
|
|
261
|
+
return { content: [{ type: 'text', text: 'No jettons found in this wallet.' }] };
|
|
262
|
+
}
|
|
263
|
+
const lines = result.balances.map((b) => {
|
|
264
|
+
const decimals = b.decimals ?? 9;
|
|
265
|
+
const raw = BigInt(b.balance);
|
|
266
|
+
const divisor = BigInt(10 ** decimals);
|
|
267
|
+
const whole = (raw / divisor).toString();
|
|
268
|
+
const frac = (raw % divisor).toString().padStart(decimals, '0').replace(/0+$/, '') || '0';
|
|
269
|
+
return `- ${b.symbol ?? b.name ?? 'Unknown'}: ${whole}.${frac} (${b.address})`;
|
|
270
|
+
});
|
|
271
|
+
return {
|
|
272
|
+
content: [{ type: 'text', text: `Jetton balances:\n${lines.join('\n')}` }],
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
catch (e) {
|
|
276
|
+
return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
server.tool('get_transactions', 'Get recent transaction history for the connected wallet.', {
|
|
280
|
+
limit: z.number().optional().describe('Number of transactions to return (default 10)'),
|
|
281
|
+
}, async ({ limit }) => {
|
|
282
|
+
if (!TOKEN) {
|
|
283
|
+
return { content: [{ type: 'text', text: 'No token configured. Use request_auth first.' }], isError: true };
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
const result = await apiCall(`/v1/wallet/transactions?limit=${limit ?? 10}`);
|
|
287
|
+
const events = result.events ?? [];
|
|
288
|
+
if (!events.length) {
|
|
289
|
+
return { content: [{ type: 'text', text: 'No recent transactions.' }] };
|
|
290
|
+
}
|
|
291
|
+
const lines = events.map((e) => {
|
|
292
|
+
const time = new Date(e.timestamp * 1000).toISOString();
|
|
293
|
+
const actions = (e.actions ?? []).map((a) => a.type).join(', ');
|
|
294
|
+
return `- ${time}: ${actions || 'unknown'} ${e.is_scam ? '[SCAM]' : ''}`;
|
|
295
|
+
});
|
|
296
|
+
return {
|
|
297
|
+
content: [{ type: 'text', text: `Recent transactions:\n${lines.join('\n')}` }],
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
catch (e) {
|
|
301
|
+
return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
server.tool('get_nft_items', 'List NFTs owned by the connected wallet.', {}, async () => {
|
|
305
|
+
if (!TOKEN) {
|
|
306
|
+
return { content: [{ type: 'text', text: 'No token configured. Use request_auth first.' }], isError: true };
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
const result = await apiCall('/v1/wallet/nfts');
|
|
310
|
+
const nfts = result.nfts ?? [];
|
|
311
|
+
if (!nfts.length) {
|
|
312
|
+
return { content: [{ type: 'text', text: 'No NFTs found in this wallet.' }] };
|
|
313
|
+
}
|
|
314
|
+
const lines = nfts.map((n) => `- ${n.name ?? 'Unnamed'} ${n.collection ? `(${n.collection})` : ''} — ${n.address}`);
|
|
315
|
+
return {
|
|
316
|
+
content: [{ type: 'text', text: `NFTs (${nfts.length}):\n${lines.join('\n')}` }],
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
catch (e) {
|
|
320
|
+
return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
server.tool('resolve_name', 'Resolve a .ton domain name to a wallet address. Use this when the user says "send to alice.ton" instead of a raw address.', {
|
|
324
|
+
domain: z.string().describe('The .ton domain name to resolve (e.g. "alice.ton")'),
|
|
325
|
+
}, async ({ domain }) => {
|
|
326
|
+
try {
|
|
327
|
+
const result = await fetch(`${API_URL}/v1/dns/${encodeURIComponent(domain)}/resolve`);
|
|
328
|
+
const data = await result.json();
|
|
329
|
+
if (!result.ok)
|
|
330
|
+
throw new Error(data.error ?? 'Failed');
|
|
331
|
+
if (!data.address) {
|
|
332
|
+
return { content: [{ type: 'text', text: `Domain "${domain}" not found or has no wallet address.` }] };
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
content: [{ type: 'text', text: `${domain} → ${data.address}` }],
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
catch (e) {
|
|
339
|
+
return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
server.tool('get_ton_price', 'Get the current price of TON in USD and other currencies.', {
|
|
343
|
+
currencies: z.string().optional().describe('Comma-separated currencies (default "USD")'),
|
|
344
|
+
}, async ({ currencies }) => {
|
|
345
|
+
try {
|
|
346
|
+
const curr = currencies || 'USD';
|
|
347
|
+
const result = await fetch(`${API_URL}/v1/market/price?tokens=TON¤cies=${curr}`);
|
|
348
|
+
const data = await result.json();
|
|
349
|
+
if (!result.ok)
|
|
350
|
+
throw new Error(data.error ?? 'Failed');
|
|
351
|
+
const tonRates = data.rates?.TON?.prices ?? {};
|
|
352
|
+
const lines = Object.entries(tonRates).map(([c, p]) => `1 TON = ${p} ${c}`);
|
|
353
|
+
return {
|
|
354
|
+
content: [{ type: 'text', text: lines.length ? lines.join('\n') : 'Price data unavailable.' }],
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
catch (e) {
|
|
358
|
+
return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
|
|
359
|
+
}
|
|
360
|
+
});
|
|
125
361
|
const transport = new StdioServerTransport();
|
|
126
362
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tongateway/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "MCP server for Agent Gateway — lets AI agents request TON blockchain transfers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/tongateway/
|
|
8
|
+
"url": "https://github.com/tongateway/mcp"
|
|
9
9
|
},
|
|
10
|
-
"keywords": [
|
|
10
|
+
"keywords": [
|
|
11
|
+
"mcp",
|
|
12
|
+
"ton",
|
|
13
|
+
"blockchain",
|
|
14
|
+
"agent",
|
|
15
|
+
"ai",
|
|
16
|
+
"wallet"
|
|
17
|
+
],
|
|
11
18
|
"type": "module",
|
|
12
19
|
"bin": {
|
|
13
20
|
"agent-gateway-mcp": "./dist/index.js"
|
|
14
21
|
},
|
|
15
|
-
"files": [
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
16
25
|
"scripts": {
|
|
17
26
|
"build": "tsc",
|
|
18
|
-
"dev": "tsx src/index.ts"
|
|
27
|
+
"dev": "tsx src/index.ts"
|
|
19
28
|
},
|
|
20
29
|
"dependencies": {
|
|
21
30
|
"@modelcontextprotocol/sdk": "^1.12.1",
|