@t2000/mcp 0.12.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 +72 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +590 -0
- package/dist/bin.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +589 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 t2000
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# @t2000/mcp
|
|
2
|
+
|
|
3
|
+
MCP server for AI agent bank accounts on Sui. Connect Claude Desktop, Cursor, or any MCP client to your t2000 agent.
|
|
4
|
+
|
|
5
|
+
**16 tools · 3 prompts · stdio transport · safeguard enforced**
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Install
|
|
11
|
+
npm i -g @t2000/cli
|
|
12
|
+
|
|
13
|
+
# Create wallet + configure safeguards
|
|
14
|
+
t2000 init
|
|
15
|
+
t2000 config set maxPerTx 100
|
|
16
|
+
t2000 config set maxDailySend 500
|
|
17
|
+
|
|
18
|
+
# Create session (saves PIN for MCP)
|
|
19
|
+
t2000 balance
|
|
20
|
+
|
|
21
|
+
# Start MCP server
|
|
22
|
+
t2000 mcp
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Platform Config
|
|
26
|
+
|
|
27
|
+
Paste into your AI platform's MCP settings:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{ "mcpServers": { "t2000": { "command": "t2000", "args": ["mcp"] } } }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Tools
|
|
34
|
+
|
|
35
|
+
| Tool | Type | Description |
|
|
36
|
+
|------|------|-------------|
|
|
37
|
+
| `t2000_balance` | read | Current balance |
|
|
38
|
+
| `t2000_address` | read | Wallet address |
|
|
39
|
+
| `t2000_positions` | read | Lending positions |
|
|
40
|
+
| `t2000_rates` | read | Interest rates |
|
|
41
|
+
| `t2000_health` | read | Health factor |
|
|
42
|
+
| `t2000_history` | read | Transaction history |
|
|
43
|
+
| `t2000_earnings` | read | Yield earnings |
|
|
44
|
+
| `t2000_send` | write | Send USDC |
|
|
45
|
+
| `t2000_save` | write | Deposit to savings |
|
|
46
|
+
| `t2000_withdraw` | write | Withdraw from savings |
|
|
47
|
+
| `t2000_borrow` | write | Borrow against collateral |
|
|
48
|
+
| `t2000_repay` | write | Repay debt |
|
|
49
|
+
| `t2000_exchange` | write | Swap assets |
|
|
50
|
+
| `t2000_rebalance` | write | Optimize yield |
|
|
51
|
+
| `t2000_config` | safety | View/set limits |
|
|
52
|
+
| `t2000_lock` | safety | Emergency freeze |
|
|
53
|
+
|
|
54
|
+
## Programmatic Usage
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { startMcpServer } from '@t2000/mcp';
|
|
58
|
+
|
|
59
|
+
await startMcpServer({ keyPath: '/path/to/key' });
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Security
|
|
63
|
+
|
|
64
|
+
- Safeguard gate prevents starting without configured limits
|
|
65
|
+
- Per-transaction and daily send caps enforced on all state-changing tools
|
|
66
|
+
- `unlock` is CLI-only — AI cannot circumvent a locked agent
|
|
67
|
+
- `dryRun: true` previews operations without signing
|
|
68
|
+
- stdio transport — private key never leaves the machine
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
MIT
|
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { readFile } from 'fs/promises';
|
|
5
|
+
import { resolve } from 'path';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
import { T2000, validateAddress, SafeguardError, T2000Error } from '@t2000/sdk';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
|
|
10
|
+
var SESSION_PATH = resolve(homedir(), ".t2000", ".session");
|
|
11
|
+
async function resolvePin() {
|
|
12
|
+
const envPin = process.env.T2000_PIN ?? process.env.T2000_PASSPHRASE;
|
|
13
|
+
if (envPin) return envPin;
|
|
14
|
+
try {
|
|
15
|
+
const session = await readFile(SESSION_PATH, "utf-8");
|
|
16
|
+
if (session.trim()) return session.trim();
|
|
17
|
+
} catch {
|
|
18
|
+
}
|
|
19
|
+
throw new Error(
|
|
20
|
+
"No PIN available. Either:\n 1. Run `t2000 balance` first (creates session), or\n 2. Set T2000_PIN environment variable"
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
async function createAgent(keyPath) {
|
|
24
|
+
const pin = await resolvePin();
|
|
25
|
+
return T2000.create({ pin, keyPath });
|
|
26
|
+
}
|
|
27
|
+
function mapError(err) {
|
|
28
|
+
if (err instanceof SafeguardError) {
|
|
29
|
+
return {
|
|
30
|
+
code: "SAFEGUARD_BLOCKED",
|
|
31
|
+
message: err.message,
|
|
32
|
+
retryable: false,
|
|
33
|
+
details: { rule: err.rule, ...err.details }
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (err instanceof T2000Error) {
|
|
37
|
+
return {
|
|
38
|
+
code: err.code,
|
|
39
|
+
message: err.message,
|
|
40
|
+
retryable: err.retryable
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
code: "UNKNOWN",
|
|
45
|
+
message: err instanceof Error ? err.message : String(err),
|
|
46
|
+
retryable: false
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function errorResult(err) {
|
|
50
|
+
const mapped = mapError(err);
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: "text", text: JSON.stringify(mapped) }],
|
|
53
|
+
isError: true
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/tools/read.ts
|
|
58
|
+
function registerReadTools(server, agent) {
|
|
59
|
+
server.tool(
|
|
60
|
+
"t2000_balance",
|
|
61
|
+
"Get agent's current balance \u2014 available (checking), savings, gas reserve, and total. All values in USD.",
|
|
62
|
+
{},
|
|
63
|
+
async () => {
|
|
64
|
+
try {
|
|
65
|
+
const result = await agent.balance();
|
|
66
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
67
|
+
} catch (err) {
|
|
68
|
+
return errorResult(err);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
server.tool(
|
|
73
|
+
"t2000_address",
|
|
74
|
+
"Get the agent's Sui wallet address.",
|
|
75
|
+
{},
|
|
76
|
+
async () => {
|
|
77
|
+
try {
|
|
78
|
+
const address = agent.address();
|
|
79
|
+
return { content: [{ type: "text", text: JSON.stringify({ address }) }] };
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return errorResult(err);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
server.tool(
|
|
86
|
+
"t2000_positions",
|
|
87
|
+
"View current lending positions across protocols (NAVI, Suilend) \u2014 deposits, borrows, APYs.",
|
|
88
|
+
{},
|
|
89
|
+
async () => {
|
|
90
|
+
try {
|
|
91
|
+
const result = await agent.positions();
|
|
92
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
93
|
+
} catch (err) {
|
|
94
|
+
return errorResult(err);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
server.tool(
|
|
99
|
+
"t2000_rates",
|
|
100
|
+
"Get best available interest rates per asset across all lending protocols.",
|
|
101
|
+
{},
|
|
102
|
+
async () => {
|
|
103
|
+
try {
|
|
104
|
+
const result = await agent.rates();
|
|
105
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
106
|
+
} catch (err) {
|
|
107
|
+
return errorResult(err);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
server.tool(
|
|
112
|
+
"t2000_health",
|
|
113
|
+
"Check the agent's health factor \u2014 measures how safe current borrows are. Below 1.0 risks liquidation.",
|
|
114
|
+
{},
|
|
115
|
+
async () => {
|
|
116
|
+
try {
|
|
117
|
+
const result = await agent.healthFactor();
|
|
118
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
119
|
+
} catch (err) {
|
|
120
|
+
return errorResult(err);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
server.tool(
|
|
125
|
+
"t2000_history",
|
|
126
|
+
"View recent transactions (sends, saves, borrows, swaps, etc.).",
|
|
127
|
+
{ limit: z.number().optional().describe("Number of transactions to return (default: 20)") },
|
|
128
|
+
async ({ limit }) => {
|
|
129
|
+
try {
|
|
130
|
+
const result = await agent.history({ limit });
|
|
131
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
132
|
+
} catch (err) {
|
|
133
|
+
return errorResult(err);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
);
|
|
137
|
+
server.tool(
|
|
138
|
+
"t2000_earnings",
|
|
139
|
+
"View yield earnings from savings positions \u2014 total earned, daily rate, current APY.",
|
|
140
|
+
{},
|
|
141
|
+
async () => {
|
|
142
|
+
try {
|
|
143
|
+
const result = await agent.earnings();
|
|
144
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
145
|
+
} catch (err) {
|
|
146
|
+
return errorResult(err);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/mutex.ts
|
|
153
|
+
var TxMutex = class {
|
|
154
|
+
queue = Promise.resolve();
|
|
155
|
+
async run(fn) {
|
|
156
|
+
let release;
|
|
157
|
+
const next = new Promise((r) => {
|
|
158
|
+
release = r;
|
|
159
|
+
});
|
|
160
|
+
const prev = this.queue;
|
|
161
|
+
this.queue = next;
|
|
162
|
+
await prev;
|
|
163
|
+
try {
|
|
164
|
+
return await fn();
|
|
165
|
+
} finally {
|
|
166
|
+
release();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// src/tools/write.ts
|
|
172
|
+
function registerWriteTools(server, agent) {
|
|
173
|
+
const mutex = new TxMutex();
|
|
174
|
+
server.tool(
|
|
175
|
+
"t2000_send",
|
|
176
|
+
"Send USDC or stablecoins to a Sui address. Amount is in dollars. Subject to per-transaction and daily send limits. Set dryRun: true to preview without signing.",
|
|
177
|
+
{
|
|
178
|
+
to: z.string().describe("Recipient Sui address (0x...)"),
|
|
179
|
+
amount: z.number().describe("Amount in dollars to send"),
|
|
180
|
+
asset: z.string().optional().describe("Asset to send (default: USDC)"),
|
|
181
|
+
dryRun: z.boolean().optional().describe("Preview without signing (default: false)")
|
|
182
|
+
},
|
|
183
|
+
async ({ to, amount, asset, dryRun }) => {
|
|
184
|
+
try {
|
|
185
|
+
if (!validateAddress(to)) {
|
|
186
|
+
return errorResult(new Error(`Invalid Sui address: ${to}`));
|
|
187
|
+
}
|
|
188
|
+
if (dryRun) {
|
|
189
|
+
agent.enforcer.check({ operation: "send", amount });
|
|
190
|
+
const balance = await agent.balance();
|
|
191
|
+
const config = agent.enforcer.getConfig();
|
|
192
|
+
return {
|
|
193
|
+
content: [{
|
|
194
|
+
type: "text",
|
|
195
|
+
text: JSON.stringify({
|
|
196
|
+
preview: true,
|
|
197
|
+
canSend: balance.available >= amount,
|
|
198
|
+
amount,
|
|
199
|
+
to,
|
|
200
|
+
asset: asset ?? "USDC",
|
|
201
|
+
currentBalance: balance.available,
|
|
202
|
+
balanceAfter: balance.available - amount,
|
|
203
|
+
safeguards: {
|
|
204
|
+
dailyUsedAfter: config.dailyUsed + amount,
|
|
205
|
+
dailyLimit: config.maxDailySend
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
}]
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const result = await mutex.run(() => agent.send({ to, amount, asset }));
|
|
212
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
213
|
+
} catch (err) {
|
|
214
|
+
return errorResult(err);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
);
|
|
218
|
+
server.tool(
|
|
219
|
+
"t2000_save",
|
|
220
|
+
'Deposit USDC to savings (earns yield). Amount is in dollars. Use "all" to save entire available balance. Set dryRun: true to preview.',
|
|
221
|
+
{
|
|
222
|
+
amount: z.union([z.number(), z.literal("all")]).describe('Dollar amount to save, or "all"'),
|
|
223
|
+
dryRun: z.boolean().optional().describe("Preview without signing (default: false)")
|
|
224
|
+
},
|
|
225
|
+
async ({ amount, dryRun }) => {
|
|
226
|
+
try {
|
|
227
|
+
if (dryRun) {
|
|
228
|
+
agent.enforcer.assertNotLocked();
|
|
229
|
+
const balance = await agent.balance();
|
|
230
|
+
const rates = await agent.rates();
|
|
231
|
+
const saveAmount = amount === "all" ? balance.available - 1 : amount;
|
|
232
|
+
return {
|
|
233
|
+
content: [{
|
|
234
|
+
type: "text",
|
|
235
|
+
text: JSON.stringify({
|
|
236
|
+
preview: true,
|
|
237
|
+
amount: saveAmount,
|
|
238
|
+
currentApy: rates.USDC?.saveApy ?? 0,
|
|
239
|
+
savingsBalanceAfter: balance.savings + saveAmount
|
|
240
|
+
})
|
|
241
|
+
}]
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const result = await mutex.run(() => agent.save({ amount }));
|
|
245
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
246
|
+
} catch (err) {
|
|
247
|
+
return errorResult(err);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
);
|
|
251
|
+
server.tool(
|
|
252
|
+
"t2000_withdraw",
|
|
253
|
+
'Withdraw from savings back to checking. Amount is in dollars. Use "all" to withdraw everything. Set dryRun: true to preview.',
|
|
254
|
+
{
|
|
255
|
+
amount: z.union([z.number(), z.literal("all")]).describe('Dollar amount to withdraw, or "all"'),
|
|
256
|
+
dryRun: z.boolean().optional().describe("Preview without signing (default: false)")
|
|
257
|
+
},
|
|
258
|
+
async ({ amount, dryRun }) => {
|
|
259
|
+
try {
|
|
260
|
+
if (dryRun) {
|
|
261
|
+
agent.enforcer.assertNotLocked();
|
|
262
|
+
const positions = await agent.positions();
|
|
263
|
+
const health = await agent.healthFactor();
|
|
264
|
+
const savings = positions.positions.filter((p) => p.type === "save").reduce((sum, p) => sum + p.amount, 0);
|
|
265
|
+
return {
|
|
266
|
+
content: [{
|
|
267
|
+
type: "text",
|
|
268
|
+
text: JSON.stringify({
|
|
269
|
+
preview: true,
|
|
270
|
+
amount: amount === "all" ? savings : amount,
|
|
271
|
+
currentSavings: savings,
|
|
272
|
+
currentHealthFactor: health.healthFactor
|
|
273
|
+
})
|
|
274
|
+
}]
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
const result = await mutex.run(() => agent.withdraw({ amount }));
|
|
278
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
279
|
+
} catch (err) {
|
|
280
|
+
return errorResult(err);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
);
|
|
284
|
+
server.tool(
|
|
285
|
+
"t2000_borrow",
|
|
286
|
+
"Borrow USDC against savings collateral. Check health factor first \u2014 below 1.0 risks liquidation. Amount is in dollars. Set dryRun: true to preview.",
|
|
287
|
+
{
|
|
288
|
+
amount: z.number().describe("Dollar amount to borrow"),
|
|
289
|
+
dryRun: z.boolean().optional().describe("Preview without signing (default: false)")
|
|
290
|
+
},
|
|
291
|
+
async ({ amount, dryRun }) => {
|
|
292
|
+
try {
|
|
293
|
+
if (dryRun) {
|
|
294
|
+
agent.enforcer.assertNotLocked();
|
|
295
|
+
const health = await agent.healthFactor();
|
|
296
|
+
const maxBorrow = await agent.maxBorrow();
|
|
297
|
+
return {
|
|
298
|
+
content: [{
|
|
299
|
+
type: "text",
|
|
300
|
+
text: JSON.stringify({
|
|
301
|
+
preview: true,
|
|
302
|
+
amount,
|
|
303
|
+
maxBorrow: maxBorrow.maxAmount,
|
|
304
|
+
currentHealthFactor: health.healthFactor,
|
|
305
|
+
estimatedHealthFactorAfter: maxBorrow.healthFactorAfter
|
|
306
|
+
})
|
|
307
|
+
}]
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
const result = await mutex.run(() => agent.borrow({ amount }));
|
|
311
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
312
|
+
} catch (err) {
|
|
313
|
+
return errorResult(err);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
);
|
|
317
|
+
server.tool(
|
|
318
|
+
"t2000_repay",
|
|
319
|
+
'Repay borrowed USDC. Amount is in dollars. Use "all" to repay entire debt. Set dryRun: true to preview.',
|
|
320
|
+
{
|
|
321
|
+
amount: z.union([z.number(), z.literal("all")]).describe('Dollar amount to repay, or "all"'),
|
|
322
|
+
dryRun: z.boolean().optional().describe("Preview without signing (default: false)")
|
|
323
|
+
},
|
|
324
|
+
async ({ amount, dryRun }) => {
|
|
325
|
+
try {
|
|
326
|
+
if (dryRun) {
|
|
327
|
+
agent.enforcer.assertNotLocked();
|
|
328
|
+
const health = await agent.healthFactor();
|
|
329
|
+
const positions = await agent.positions();
|
|
330
|
+
const totalDebt = positions.positions.filter((p) => p.type === "borrow").reduce((sum, p) => sum + p.amount, 0);
|
|
331
|
+
return {
|
|
332
|
+
content: [{
|
|
333
|
+
type: "text",
|
|
334
|
+
text: JSON.stringify({
|
|
335
|
+
preview: true,
|
|
336
|
+
amount: amount === "all" ? totalDebt : amount,
|
|
337
|
+
currentDebt: totalDebt,
|
|
338
|
+
currentHealthFactor: health.healthFactor
|
|
339
|
+
})
|
|
340
|
+
}]
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const result = await mutex.run(() => agent.repay({ amount }));
|
|
344
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
345
|
+
} catch (err) {
|
|
346
|
+
return errorResult(err);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
);
|
|
350
|
+
server.tool(
|
|
351
|
+
"t2000_exchange",
|
|
352
|
+
"Swap assets via Cetus DEX (e.g. USDC to SUI, SUI to USDC). Amount is in source asset units. Set dryRun: true to get a quote without executing.",
|
|
353
|
+
{
|
|
354
|
+
amount: z.number().describe("Amount to swap (in source asset units)"),
|
|
355
|
+
from: z.string().describe("Source asset (e.g. USDC, SUI)"),
|
|
356
|
+
to: z.string().describe("Target asset (e.g. SUI, USDC)"),
|
|
357
|
+
maxSlippage: z.number().optional().describe("Max slippage percentage (default: 3%)"),
|
|
358
|
+
dryRun: z.boolean().optional().describe("Preview without signing (default: false)")
|
|
359
|
+
},
|
|
360
|
+
async ({ amount, from, to, maxSlippage, dryRun }) => {
|
|
361
|
+
try {
|
|
362
|
+
if (dryRun) {
|
|
363
|
+
agent.enforcer.assertNotLocked();
|
|
364
|
+
const quote = await agent.exchangeQuote({ from, to, amount });
|
|
365
|
+
return {
|
|
366
|
+
content: [{
|
|
367
|
+
type: "text",
|
|
368
|
+
text: JSON.stringify({
|
|
369
|
+
preview: true,
|
|
370
|
+
from,
|
|
371
|
+
to,
|
|
372
|
+
amount,
|
|
373
|
+
expectedOutput: quote.expectedOutput,
|
|
374
|
+
priceImpact: quote.priceImpact,
|
|
375
|
+
fee: quote.fee.amount
|
|
376
|
+
})
|
|
377
|
+
}]
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const result = await mutex.run(
|
|
381
|
+
() => agent.exchange({ from, to, amount, maxSlippage })
|
|
382
|
+
);
|
|
383
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
384
|
+
} catch (err) {
|
|
385
|
+
return errorResult(err);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
);
|
|
389
|
+
server.tool(
|
|
390
|
+
"t2000_rebalance",
|
|
391
|
+
"Optimize yield by moving funds to the highest-rate protocol. Always previews first \u2014 set dryRun: false to execute. Shows plan with expected APY gain and break-even period.",
|
|
392
|
+
{
|
|
393
|
+
dryRun: z.boolean().optional().describe("Preview without executing (default: true)"),
|
|
394
|
+
minYieldDiff: z.number().optional().describe("Min APY difference to rebalance (default: 0.5%)"),
|
|
395
|
+
maxBreakEven: z.number().optional().describe("Max break-even days (default: 30)")
|
|
396
|
+
},
|
|
397
|
+
async ({ dryRun, minYieldDiff, maxBreakEven }) => {
|
|
398
|
+
try {
|
|
399
|
+
const result = await mutex.run(
|
|
400
|
+
() => agent.rebalance({
|
|
401
|
+
dryRun: dryRun ?? true,
|
|
402
|
+
minYieldDiff,
|
|
403
|
+
maxBreakEven
|
|
404
|
+
})
|
|
405
|
+
);
|
|
406
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
407
|
+
} catch (err) {
|
|
408
|
+
return errorResult(err);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
function registerSafetyTools(server, agent) {
|
|
414
|
+
server.tool(
|
|
415
|
+
"t2000_config",
|
|
416
|
+
'View or set agent safeguard limits (per-transaction max, daily send limit). Use action "show" to view current limits, "set" to update. Values are in dollars. Set to 0 for unlimited.',
|
|
417
|
+
{
|
|
418
|
+
action: z.enum(["show", "set"]).describe('"show" to view current limits, "set" to update a limit'),
|
|
419
|
+
key: z.string().optional().describe('Setting to update: "maxPerTx" or "maxDailySend"'),
|
|
420
|
+
value: z.number().optional().describe("New value in dollars (0 = unlimited)")
|
|
421
|
+
},
|
|
422
|
+
async ({ action, key, value }) => {
|
|
423
|
+
try {
|
|
424
|
+
if (action === "show") {
|
|
425
|
+
const config = agent.enforcer.getConfig();
|
|
426
|
+
return {
|
|
427
|
+
content: [{
|
|
428
|
+
type: "text",
|
|
429
|
+
text: JSON.stringify({
|
|
430
|
+
locked: config.locked,
|
|
431
|
+
maxPerTx: config.maxPerTx,
|
|
432
|
+
maxDailySend: config.maxDailySend,
|
|
433
|
+
dailyUsed: config.dailyUsed
|
|
434
|
+
})
|
|
435
|
+
}]
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
if (!key || value === void 0) {
|
|
439
|
+
return errorResult(new Error('Both "key" and "value" are required for action "set"'));
|
|
440
|
+
}
|
|
441
|
+
if (key === "locked") {
|
|
442
|
+
return errorResult(new Error('Cannot set "locked" via config. Use t2000_lock to freeze operations.'));
|
|
443
|
+
}
|
|
444
|
+
if (key !== "maxPerTx" && key !== "maxDailySend") {
|
|
445
|
+
return errorResult(new Error(`Unknown key "${key}". Valid keys: "maxPerTx", "maxDailySend"`));
|
|
446
|
+
}
|
|
447
|
+
if (value < 0) {
|
|
448
|
+
return errorResult(new Error("Value must be a non-negative number"));
|
|
449
|
+
}
|
|
450
|
+
agent.enforcer.set(key, value);
|
|
451
|
+
return {
|
|
452
|
+
content: [{
|
|
453
|
+
type: "text",
|
|
454
|
+
text: JSON.stringify({ updated: true, key, value })
|
|
455
|
+
}]
|
|
456
|
+
};
|
|
457
|
+
} catch (err) {
|
|
458
|
+
return errorResult(err);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
);
|
|
462
|
+
server.tool(
|
|
463
|
+
"t2000_lock",
|
|
464
|
+
"Freeze all agent operations immediately. Only a human can unlock via `t2000 unlock` in the terminal. Use this as an emergency stop.",
|
|
465
|
+
{},
|
|
466
|
+
async () => {
|
|
467
|
+
try {
|
|
468
|
+
agent.enforcer.lock();
|
|
469
|
+
return {
|
|
470
|
+
content: [{
|
|
471
|
+
type: "text",
|
|
472
|
+
text: JSON.stringify({
|
|
473
|
+
locked: true,
|
|
474
|
+
message: "Agent locked. Only a human can unlock via: t2000 unlock"
|
|
475
|
+
})
|
|
476
|
+
}]
|
|
477
|
+
};
|
|
478
|
+
} catch (err) {
|
|
479
|
+
return errorResult(err);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
function registerPrompts(server) {
|
|
485
|
+
server.prompt(
|
|
486
|
+
"financial-report",
|
|
487
|
+
"Get a comprehensive summary of the agent's financial position \u2014 balance, savings, debt, health factor, and yield earnings.",
|
|
488
|
+
async () => ({
|
|
489
|
+
messages: [{
|
|
490
|
+
role: "user",
|
|
491
|
+
content: {
|
|
492
|
+
type: "text",
|
|
493
|
+
text: [
|
|
494
|
+
"You are a financial assistant for a t2000 AI agent bank account.",
|
|
495
|
+
"",
|
|
496
|
+
"Please provide a comprehensive financial report by:",
|
|
497
|
+
"1. Check the current balance (t2000_balance)",
|
|
498
|
+
"2. Review lending positions (t2000_positions)",
|
|
499
|
+
"3. Check the health factor (t2000_health)",
|
|
500
|
+
"4. Show yield earnings (t2000_earnings)",
|
|
501
|
+
"5. Review current interest rates (t2000_rates)",
|
|
502
|
+
"",
|
|
503
|
+
"Summarize the agent's financial health in a clear, concise format with actionable recommendations."
|
|
504
|
+
].join("\n")
|
|
505
|
+
}
|
|
506
|
+
}]
|
|
507
|
+
})
|
|
508
|
+
);
|
|
509
|
+
server.prompt(
|
|
510
|
+
"optimize-yield",
|
|
511
|
+
"Analyze savings positions and suggest yield optimizations \u2014 rate comparisons, rebalancing opportunities.",
|
|
512
|
+
async () => ({
|
|
513
|
+
messages: [{
|
|
514
|
+
role: "user",
|
|
515
|
+
content: {
|
|
516
|
+
type: "text",
|
|
517
|
+
text: [
|
|
518
|
+
"You are a yield optimization assistant for a t2000 AI agent bank account.",
|
|
519
|
+
"",
|
|
520
|
+
"Please analyze the current yield strategy:",
|
|
521
|
+
"1. Check current positions (t2000_positions)",
|
|
522
|
+
"2. Compare rates across protocols (t2000_rates)",
|
|
523
|
+
"3. Run a dry-run rebalance to see if optimization is available (t2000_rebalance with dryRun: true)",
|
|
524
|
+
"",
|
|
525
|
+
"If a rebalance would improve yield, explain the trade-off (gas cost vs yield gain, break-even period) and ask if the user wants to proceed."
|
|
526
|
+
].join("\n")
|
|
527
|
+
}
|
|
528
|
+
}]
|
|
529
|
+
})
|
|
530
|
+
);
|
|
531
|
+
server.prompt(
|
|
532
|
+
"send-money",
|
|
533
|
+
"Guided flow for sending USDC to a Sui address \u2014 validates address, checks limits, previews before signing.",
|
|
534
|
+
{
|
|
535
|
+
to: z.string().optional().describe("Recipient Sui address"),
|
|
536
|
+
amount: z.number().optional().describe("Amount in dollars")
|
|
537
|
+
},
|
|
538
|
+
async ({ to, amount }) => {
|
|
539
|
+
const context = [
|
|
540
|
+
to ? `Recipient address: ${to}` : "",
|
|
541
|
+
amount ? `Amount: $${amount}` : ""
|
|
542
|
+
].filter(Boolean).join("\n");
|
|
543
|
+
return {
|
|
544
|
+
messages: [{
|
|
545
|
+
role: "user",
|
|
546
|
+
content: {
|
|
547
|
+
type: "text",
|
|
548
|
+
text: [
|
|
549
|
+
"You are a payment assistant for a t2000 AI agent bank account.",
|
|
550
|
+
"",
|
|
551
|
+
context ? `Context:
|
|
552
|
+
${context}
|
|
553
|
+
` : "",
|
|
554
|
+
"The user wants to send money. Follow this flow:",
|
|
555
|
+
"1. If address or amount is missing, ask the user",
|
|
556
|
+
"2. Preview the transaction (t2000_send with dryRun: true)",
|
|
557
|
+
"3. Show the preview \u2014 amount, recipient, remaining balance, safeguard status",
|
|
558
|
+
"4. Ask the user to confirm before executing",
|
|
559
|
+
"5. Execute the send (t2000_send with dryRun: false)",
|
|
560
|
+
"6. Show the transaction result with the Suiscan link"
|
|
561
|
+
].join("\n")
|
|
562
|
+
}
|
|
563
|
+
}]
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/index.ts
|
|
570
|
+
async function startMcpServer(opts) {
|
|
571
|
+
const agent = await createAgent(opts?.keyPath);
|
|
572
|
+
if (!agent.enforcer.isConfigured()) {
|
|
573
|
+
console.error(
|
|
574
|
+
"Safeguards not configured. Set limits before starting MCP:\n t2000 config set maxPerTx 100\n t2000 config set maxDailySend 500\n"
|
|
575
|
+
);
|
|
576
|
+
process.exit(1);
|
|
577
|
+
}
|
|
578
|
+
const server = new McpServer({ name: "t2000", version: "0.12.0" });
|
|
579
|
+
registerReadTools(server, agent);
|
|
580
|
+
registerWriteTools(server, agent);
|
|
581
|
+
registerSafetyTools(server, agent);
|
|
582
|
+
registerPrompts(server);
|
|
583
|
+
const transport = new StdioServerTransport();
|
|
584
|
+
await server.connect(transport);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// src/bin.ts
|
|
588
|
+
await startMcpServer();
|
|
589
|
+
//# sourceMappingURL=bin.js.map
|
|
590
|
+
//# sourceMappingURL=bin.js.map
|