@grinta-mcp/server 0.2.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 +49 -0
- package/dist/config.d.ts +38 -0
- package/dist/config.js +45 -0
- package/dist/grinta.d.ts +99 -0
- package/dist/grinta.js +561 -0
- package/dist/identity.d.ts +24 -0
- package/dist/identity.js +74 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +284 -0
- package/dist/mcp.d.ts +8 -0
- package/dist/mcp.js +322 -0
- package/dist/price-feed.d.ts +23 -0
- package/dist/price-feed.js +151 -0
- package/dist/scan-safes.d.ts +5 -0
- package/dist/scan-safes.js +87 -0
- package/dist/swap.d.ts +25 -0
- package/dist/swap.js +271 -0
- package/dist/test-swap.d.ts +5 -0
- package/dist/test-swap.js +93 -0
- package/dist/utils.d.ts +19 -0
- package/dist/utils.js +35 -0
- package/package.json +35 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* grinta-agent - Starknet AI Agent for Grinta CDP Protocol
|
|
3
|
+
*
|
|
4
|
+
* Full agent with:
|
|
5
|
+
* - Wallet operations
|
|
6
|
+
* - DeFi via AVNU
|
|
7
|
+
* - On-chain identity (ERC-8004)
|
|
8
|
+
* - Grinta CDP management (open SAFEs, borrow GRIT, monitor health)
|
|
9
|
+
*/
|
|
10
|
+
import { type AgentIdentity } from "./identity.js";
|
|
11
|
+
import { GrintaClient } from "./grinta.js";
|
|
12
|
+
declare class GrintaAgent {
|
|
13
|
+
private provider;
|
|
14
|
+
private account;
|
|
15
|
+
private identityClient;
|
|
16
|
+
private identity;
|
|
17
|
+
private grinta;
|
|
18
|
+
private isRunning;
|
|
19
|
+
private managedSafeIds;
|
|
20
|
+
constructor();
|
|
21
|
+
get address(): string;
|
|
22
|
+
/**
|
|
23
|
+
* Start the agent
|
|
24
|
+
*/
|
|
25
|
+
start(): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Stop the agent
|
|
28
|
+
*/
|
|
29
|
+
stop(): void;
|
|
30
|
+
/**
|
|
31
|
+
* Load on-chain identity
|
|
32
|
+
*/
|
|
33
|
+
private loadIdentity;
|
|
34
|
+
/**
|
|
35
|
+
* Check wallet balances (ETH, WBTC, GRIT)
|
|
36
|
+
*/
|
|
37
|
+
private checkBalances;
|
|
38
|
+
/**
|
|
39
|
+
* Display Grinta system status
|
|
40
|
+
*/
|
|
41
|
+
private showSystemStatus;
|
|
42
|
+
/**
|
|
43
|
+
* Display a SAFE's health
|
|
44
|
+
*/
|
|
45
|
+
private showSafeHealth;
|
|
46
|
+
/**
|
|
47
|
+
* Main monitoring loop
|
|
48
|
+
*/
|
|
49
|
+
private monitorLoop;
|
|
50
|
+
/**
|
|
51
|
+
* Run one agent cycle: check health of all managed SAFEs
|
|
52
|
+
*/
|
|
53
|
+
private runCycle;
|
|
54
|
+
/**
|
|
55
|
+
* Monitor a single SAFE and take action if needed
|
|
56
|
+
*/
|
|
57
|
+
private monitorSafe;
|
|
58
|
+
/**
|
|
59
|
+
* Emergency repay to bring LTV back to target
|
|
60
|
+
*/
|
|
61
|
+
private emergencyRepay;
|
|
62
|
+
/**
|
|
63
|
+
* Execute a swap via Ekubo Router
|
|
64
|
+
*/
|
|
65
|
+
swap(sellToken: string, buyToken: string, humanAmount: string): Promise<string | null>;
|
|
66
|
+
/**
|
|
67
|
+
* Get agent info for A2A
|
|
68
|
+
*/
|
|
69
|
+
getAgentCard(): {
|
|
70
|
+
name: string;
|
|
71
|
+
version: string;
|
|
72
|
+
capabilities: string[];
|
|
73
|
+
address: string;
|
|
74
|
+
identity: AgentIdentity | null;
|
|
75
|
+
managedSafes: number[];
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Expose the Grinta client for direct use
|
|
79
|
+
*/
|
|
80
|
+
get grintaClient(): GrintaClient;
|
|
81
|
+
}
|
|
82
|
+
export default GrintaAgent;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* grinta-agent - Starknet AI Agent for Grinta CDP Protocol
|
|
3
|
+
*
|
|
4
|
+
* Full agent with:
|
|
5
|
+
* - Wallet operations
|
|
6
|
+
* - DeFi via AVNU
|
|
7
|
+
* - On-chain identity (ERC-8004)
|
|
8
|
+
* - Grinta CDP management (open SAFEs, borrow GRIT, monitor health)
|
|
9
|
+
*/
|
|
10
|
+
// Load .env BEFORE any other imports that read process.env
|
|
11
|
+
import dotenv from "dotenv";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
import { dirname, join } from "path";
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
dotenv.config({ path: join(__dirname, "..", ".env") });
|
|
16
|
+
import { Account, RpcProvider, Contract } from "starknet";
|
|
17
|
+
import { CONFIG, TOKENS, AGENT_METADATA } from "./config.js";
|
|
18
|
+
import { IdentityClient } from "./identity.js";
|
|
19
|
+
import { GrintaClient, WAD } from "./grinta.js";
|
|
20
|
+
import { ekuboSwap } from "./swap.js";
|
|
21
|
+
import { formatAmount, sleep } from "./utils.js";
|
|
22
|
+
// ERC20 ABI
|
|
23
|
+
const ERC20_ABI = [
|
|
24
|
+
{
|
|
25
|
+
type: "interface",
|
|
26
|
+
name: "openzeppelin::token::erc20::interface::IERC20",
|
|
27
|
+
items: [
|
|
28
|
+
{
|
|
29
|
+
type: "function",
|
|
30
|
+
name: "balance_of",
|
|
31
|
+
inputs: [{ name: "account", type: "core::starknet::contract_address::ContractAddress" }],
|
|
32
|
+
outputs: [{ type: "core::integer::u256" }],
|
|
33
|
+
state_mutability: "view",
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
// --- Health Management Config ---
|
|
39
|
+
const TARGET_LTV = 0.40; // 40%
|
|
40
|
+
const DANGER_LTV = 0.60; // 60%
|
|
41
|
+
const CRITICAL_LTV = 0.62; // 62% (approaching 66.7% liquidation at 150% ratio)
|
|
42
|
+
class GrintaAgent {
|
|
43
|
+
provider;
|
|
44
|
+
account;
|
|
45
|
+
identityClient = null;
|
|
46
|
+
identity = null;
|
|
47
|
+
grinta;
|
|
48
|
+
isRunning = false;
|
|
49
|
+
managedSafeIds = [];
|
|
50
|
+
constructor() {
|
|
51
|
+
this.provider = new RpcProvider({ nodeUrl: CONFIG.RPC_URL });
|
|
52
|
+
this.account = new Account({
|
|
53
|
+
provider: this.provider,
|
|
54
|
+
address: CONFIG.ACCOUNT_ADDRESS,
|
|
55
|
+
signer: CONFIG.PRIVATE_KEY,
|
|
56
|
+
});
|
|
57
|
+
// Initialize Grinta CDP client
|
|
58
|
+
this.grinta = new GrintaClient(this.account, this.provider);
|
|
59
|
+
// Initialize identity client if registry configured
|
|
60
|
+
if (CONFIG.IDENTITY_REGISTRY_ADDRESS) {
|
|
61
|
+
this.identityClient = new IdentityClient(CONFIG.IDENTITY_REGISTRY_ADDRESS, this.provider);
|
|
62
|
+
}
|
|
63
|
+
// Load managed SAFE IDs from env
|
|
64
|
+
const safeIds = process.env.GRINTA_SAFE_IDS;
|
|
65
|
+
if (safeIds) {
|
|
66
|
+
this.managedSafeIds = safeIds.split(",").map((id) => parseInt(id.trim(), 10));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
get address() {
|
|
70
|
+
return this.account.address;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Start the agent
|
|
74
|
+
*/
|
|
75
|
+
async start() {
|
|
76
|
+
console.log("Grinta Agent Starting...");
|
|
77
|
+
console.log(`Address: ${this.address}`);
|
|
78
|
+
console.log(`Agent: ${AGENT_METADATA.name} v${AGENT_METADATA.version}`);
|
|
79
|
+
await this.checkBalances();
|
|
80
|
+
await this.showSystemStatus();
|
|
81
|
+
if (this.managedSafeIds.length > 0) {
|
|
82
|
+
console.log(`\nManaged SAFEs: ${this.managedSafeIds.join(", ")}`);
|
|
83
|
+
for (const safeId of this.managedSafeIds) {
|
|
84
|
+
await this.showSafeHealth(safeId);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
console.log("\nNo managed SAFEs configured. Set GRINTA_SAFE_IDS in .env");
|
|
89
|
+
}
|
|
90
|
+
if (this.identityClient) {
|
|
91
|
+
await this.loadIdentity();
|
|
92
|
+
}
|
|
93
|
+
this.isRunning = true;
|
|
94
|
+
console.log(`\nAgent ready. Monitoring every ${CONFIG.CHECK_INTERVAL_MS / 1000}s\n`);
|
|
95
|
+
this.monitorLoop();
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Stop the agent
|
|
99
|
+
*/
|
|
100
|
+
stop() {
|
|
101
|
+
this.isRunning = false;
|
|
102
|
+
console.log("Agent stopped");
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Load on-chain identity
|
|
106
|
+
*/
|
|
107
|
+
async loadIdentity() {
|
|
108
|
+
if (!this.identityClient)
|
|
109
|
+
return;
|
|
110
|
+
console.log("Loading on-chain identity...");
|
|
111
|
+
console.log(" Identity system available (configure IDENTITY_REGISTRY_ADDRESS)");
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Check wallet balances (ETH, WBTC, GRIT)
|
|
115
|
+
*/
|
|
116
|
+
async checkBalances() {
|
|
117
|
+
console.log("\n--- Wallet Balances ---");
|
|
118
|
+
const ethContract = new Contract({
|
|
119
|
+
abi: ERC20_ABI,
|
|
120
|
+
address: TOKENS.ETH,
|
|
121
|
+
providerOrAccount: this.provider,
|
|
122
|
+
});
|
|
123
|
+
try {
|
|
124
|
+
const [ethBal, wbtcBal, gritBal] = await Promise.all([
|
|
125
|
+
ethContract.balance_of(this.address).then((r) => BigInt(r)),
|
|
126
|
+
this.grinta.getWbtcBalance(this.address),
|
|
127
|
+
this.grinta.getGritBalance(this.address),
|
|
128
|
+
]);
|
|
129
|
+
console.log(` ETH: ${formatAmount(ethBal, 18)} ETH`);
|
|
130
|
+
console.log(` WBTC: ${formatAmount(wbtcBal, 8)} WBTC`);
|
|
131
|
+
console.log(` GRIT: ${formatAmount(gritBal, 18)} GRIT`);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
console.error(" Error fetching balances:", error);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Display Grinta system status
|
|
139
|
+
*/
|
|
140
|
+
async showSystemStatus() {
|
|
141
|
+
console.log("\n--- Grinta System Status ---");
|
|
142
|
+
try {
|
|
143
|
+
const status = await this.grinta.getSystemStatus();
|
|
144
|
+
console.log(this.grinta.formatSystemStatus(status));
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
console.error(" Error fetching system status:", error);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Display a SAFE's health
|
|
152
|
+
*/
|
|
153
|
+
async showSafeHealth(safeId) {
|
|
154
|
+
console.log(`\n--- SAFE #${safeId} Health ---`);
|
|
155
|
+
try {
|
|
156
|
+
const health = await this.grinta.getPositionHealth(safeId);
|
|
157
|
+
console.log(this.grinta.formatHealth(health));
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
console.error(` Error fetching SAFE #${safeId} health:`, error);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Main monitoring loop
|
|
165
|
+
*/
|
|
166
|
+
async monitorLoop() {
|
|
167
|
+
while (this.isRunning) {
|
|
168
|
+
try {
|
|
169
|
+
await this.runCycle();
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
console.error("Cycle error:", error);
|
|
173
|
+
}
|
|
174
|
+
await sleep(CONFIG.CHECK_INTERVAL_MS);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Run one agent cycle: check health of all managed SAFEs
|
|
179
|
+
*/
|
|
180
|
+
async runCycle() {
|
|
181
|
+
const now = new Date().toLocaleTimeString();
|
|
182
|
+
console.log(`[${now}] Running cycle...`);
|
|
183
|
+
for (const safeId of this.managedSafeIds) {
|
|
184
|
+
await this.monitorSafe(safeId);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Monitor a single SAFE and take action if needed
|
|
189
|
+
*/
|
|
190
|
+
async monitorSafe(safeId) {
|
|
191
|
+
try {
|
|
192
|
+
const health = await this.grinta.getPositionHealth(safeId);
|
|
193
|
+
const currentLtv = Number(health.ltv) / Number(WAD);
|
|
194
|
+
if (health.debt === 0n) {
|
|
195
|
+
console.log(` SAFE #${safeId}: empty (no debt)`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
console.log(` SAFE #${safeId}: LTV ${(currentLtv * 100).toFixed(2)}%`);
|
|
199
|
+
if (currentLtv > CRITICAL_LTV) {
|
|
200
|
+
console.log(` ⚠ CRITICAL: SAFE #${safeId} LTV ${(currentLtv * 100).toFixed(1)}% > ${CRITICAL_LTV * 100}%`);
|
|
201
|
+
await this.emergencyRepay(safeId, health);
|
|
202
|
+
}
|
|
203
|
+
else if (currentLtv > DANGER_LTV) {
|
|
204
|
+
console.log(` ⚠ WARNING: SAFE #${safeId} LTV ${(currentLtv * 100).toFixed(1)}% > ${DANGER_LTV * 100}%`);
|
|
205
|
+
// Could auto-deposit or repay here
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
console.error(` Error monitoring SAFE #${safeId}:`, error);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Emergency repay to bring LTV back to target
|
|
214
|
+
*/
|
|
215
|
+
async emergencyRepay(safeId, health) {
|
|
216
|
+
// Calculate how much to repay to reach target LTV
|
|
217
|
+
// target_ltv = (debt - repay) / collateral_value
|
|
218
|
+
// repay = debt - target_ltv * collateral_value
|
|
219
|
+
const targetDebt = (health.collateralValue * BigInt(Math.floor(TARGET_LTV * 1e18))) / WAD;
|
|
220
|
+
const repayAmount = health.debt - targetDebt;
|
|
221
|
+
if (repayAmount <= 0n)
|
|
222
|
+
return;
|
|
223
|
+
const gritBalance = await this.grinta.getGritBalance(this.address);
|
|
224
|
+
const actualRepay = repayAmount > gritBalance ? gritBalance : repayAmount;
|
|
225
|
+
if (actualRepay <= 0n) {
|
|
226
|
+
console.log(` No GRIT available to repay. Need ${formatAmount(repayAmount, 18)} GRIT`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
console.log(` Repaying ${formatAmount(actualRepay, 18)} GRIT on SAFE #${safeId}...`);
|
|
230
|
+
await this.grinta.repay(safeId, actualRepay);
|
|
231
|
+
console.log(` Repayment complete.`);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Execute a swap via Ekubo Router
|
|
235
|
+
*/
|
|
236
|
+
async swap(sellToken, buyToken, humanAmount) {
|
|
237
|
+
try {
|
|
238
|
+
const { txHash } = await ekuboSwap(this.account, this.provider, sellToken, buyToken, humanAmount);
|
|
239
|
+
return txHash;
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
console.error("Swap error:", error);
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Get agent info for A2A
|
|
248
|
+
*/
|
|
249
|
+
getAgentCard() {
|
|
250
|
+
return {
|
|
251
|
+
name: AGENT_METADATA.name,
|
|
252
|
+
version: AGENT_METADATA.version,
|
|
253
|
+
capabilities: AGENT_METADATA.capabilities,
|
|
254
|
+
address: this.address,
|
|
255
|
+
identity: this.identity,
|
|
256
|
+
managedSafes: this.managedSafeIds,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Expose the Grinta client for direct use
|
|
261
|
+
*/
|
|
262
|
+
get grintaClient() {
|
|
263
|
+
return this.grinta;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async function main() {
|
|
267
|
+
if (!CONFIG.ACCOUNT_ADDRESS || !CONFIG.PRIVATE_KEY) {
|
|
268
|
+
console.error("Missing environment variables!");
|
|
269
|
+
console.error("Please configure .env with STARKNET_ACCOUNT_ADDRESS and STARKNET_PRIVATE_KEY");
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
const agent = new GrintaAgent();
|
|
273
|
+
process.on("SIGINT", () => {
|
|
274
|
+
console.log("\nShutting down...");
|
|
275
|
+
agent.stop();
|
|
276
|
+
process.exit(0);
|
|
277
|
+
});
|
|
278
|
+
await agent.start();
|
|
279
|
+
}
|
|
280
|
+
main().catch((error) => {
|
|
281
|
+
console.error("Fatal error:", error);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
});
|
|
284
|
+
export default GrintaAgent;
|
package/dist/mcp.d.ts
ADDED
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Grinta CDP MCP Server (dev mode — signs and executes directly)
|
|
4
|
+
*
|
|
5
|
+
* Exposes all Grinta protocol operations as MCP tools.
|
|
6
|
+
* Run: npx tsx src/mcp.ts
|
|
7
|
+
*/
|
|
8
|
+
import dotenv from "dotenv";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
import { dirname, join } from "path";
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
dotenv.config({ path: join(__dirname, "..", ".env") });
|
|
13
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
14
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
|
+
import { ListToolsRequestSchema, CallToolRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
16
|
+
import { Account, RpcProvider, Contract } from "starknet";
|
|
17
|
+
import { CONFIG, TOKENS } from "./config.js";
|
|
18
|
+
import { GrintaClient, WAD } from "./grinta.js";
|
|
19
|
+
import { ekuboSwap } from "./swap.js";
|
|
20
|
+
import { updateBtcPrice } from "./price-feed.js";
|
|
21
|
+
import { formatAmount, parseAmount } from "./utils.js";
|
|
22
|
+
// --- Starknet setup ---
|
|
23
|
+
const provider = new RpcProvider({ nodeUrl: CONFIG.RPC_URL });
|
|
24
|
+
const account = new Account({
|
|
25
|
+
provider,
|
|
26
|
+
address: CONFIG.ACCOUNT_ADDRESS,
|
|
27
|
+
signer: CONFIG.PRIVATE_KEY,
|
|
28
|
+
});
|
|
29
|
+
const grinta = new GrintaClient(account, provider);
|
|
30
|
+
// ERC20 for balance reads
|
|
31
|
+
const ERC20_ABI = [
|
|
32
|
+
{
|
|
33
|
+
type: "interface",
|
|
34
|
+
name: "IERC20",
|
|
35
|
+
items: [
|
|
36
|
+
{
|
|
37
|
+
type: "function",
|
|
38
|
+
name: "balance_of",
|
|
39
|
+
inputs: [{ name: "account", type: "core::starknet::contract_address::ContractAddress" }],
|
|
40
|
+
outputs: [{ type: "core::integer::u256" }],
|
|
41
|
+
state_mutability: "view",
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
// --- Tool definitions ---
|
|
47
|
+
const TOOLS = [
|
|
48
|
+
// Read
|
|
49
|
+
{
|
|
50
|
+
name: "grinta_get_system_status",
|
|
51
|
+
description: "Get full system status: BTC/USD price, GRIT market price, redemption price/rate, total debt, collateral, debt ceiling, liquidation ratio.",
|
|
52
|
+
inputSchema: { type: "object", properties: {} },
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "grinta_get_position_health",
|
|
56
|
+
description: "Get a SAFE's health: collateral value, debt, LTV, liquidation price.",
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: "object",
|
|
59
|
+
properties: { safe_id: { type: "string", description: "SAFE ID (e.g. '1')" } },
|
|
60
|
+
required: ["safe_id"],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "grinta_get_safe",
|
|
65
|
+
description: "Get raw SAFE data: collateral (WAD) and debt (WAD).",
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: { safe_id: { type: "string", description: "SAFE ID" } },
|
|
69
|
+
required: ["safe_id"],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "grinta_get_max_borrow",
|
|
74
|
+
description: "Get the maximum additional GRIT that can be borrowed from a SAFE.",
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: "object",
|
|
77
|
+
properties: { safe_id: { type: "string", description: "SAFE ID" } },
|
|
78
|
+
required: ["safe_id"],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "grinta_get_balances",
|
|
83
|
+
description: "Get wallet balances for ETH, WBTC, GRIT, USDC. Defaults to the agent's address.",
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: { address: { type: "string", description: "Starknet address (optional, defaults to agent)" } },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
// CDP writes
|
|
90
|
+
{
|
|
91
|
+
name: "grinta_open_safe",
|
|
92
|
+
description: "Open a new empty SAFE. Returns the transaction hash.",
|
|
93
|
+
inputSchema: { type: "object", properties: {} },
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "grinta_deposit",
|
|
97
|
+
description: "Deposit WBTC collateral into a SAFE. Amount in BTC (e.g. '0.5').",
|
|
98
|
+
inputSchema: {
|
|
99
|
+
type: "object",
|
|
100
|
+
properties: {
|
|
101
|
+
safe_id: { type: "string", description: "SAFE ID" },
|
|
102
|
+
amount: { type: "string", description: "WBTC amount in BTC (e.g. '0.5')" },
|
|
103
|
+
},
|
|
104
|
+
required: ["safe_id", "amount"],
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "grinta_withdraw",
|
|
109
|
+
description: "Withdraw WBTC collateral from a SAFE. Amount in BTC (WAD units, e.g. '0.5').",
|
|
110
|
+
inputSchema: {
|
|
111
|
+
type: "object",
|
|
112
|
+
properties: {
|
|
113
|
+
safe_id: { type: "string", description: "SAFE ID" },
|
|
114
|
+
amount: { type: "string", description: "WBTC amount in BTC (e.g. '0.5')" },
|
|
115
|
+
},
|
|
116
|
+
required: ["safe_id", "amount"],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "grinta_borrow",
|
|
121
|
+
description: "Borrow GRIT against existing collateral in a SAFE. Amount in GRIT (e.g. '1000').",
|
|
122
|
+
inputSchema: {
|
|
123
|
+
type: "object",
|
|
124
|
+
properties: {
|
|
125
|
+
safe_id: { type: "string", description: "SAFE ID" },
|
|
126
|
+
amount: { type: "string", description: "GRIT amount (e.g. '1000')" },
|
|
127
|
+
},
|
|
128
|
+
required: ["safe_id", "amount"],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: "grinta_repay",
|
|
133
|
+
description: "Repay GRIT debt on a SAFE. Amount in GRIT (e.g. '500').",
|
|
134
|
+
inputSchema: {
|
|
135
|
+
type: "object",
|
|
136
|
+
properties: {
|
|
137
|
+
safe_id: { type: "string", description: "SAFE ID" },
|
|
138
|
+
amount: { type: "string", description: "GRIT amount (e.g. '500')" },
|
|
139
|
+
},
|
|
140
|
+
required: ["safe_id", "amount"],
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "grinta_close_safe",
|
|
145
|
+
description: "Close a SAFE. Requires zero debt. Returns remaining collateral.",
|
|
146
|
+
inputSchema: {
|
|
147
|
+
type: "object",
|
|
148
|
+
properties: { safe_id: { type: "string", description: "SAFE ID" } },
|
|
149
|
+
required: ["safe_id"],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
// DEX
|
|
153
|
+
{
|
|
154
|
+
name: "grinta_swap",
|
|
155
|
+
description: "Swap tokens via Ekubo DEX. Supported: USDC, GRIT. Example: sell_token='GRIT', buy_token='USDC', amount='100'.",
|
|
156
|
+
inputSchema: {
|
|
157
|
+
type: "object",
|
|
158
|
+
properties: {
|
|
159
|
+
sell_token: { type: "string", description: "Token to sell (e.g. 'GRIT', 'USDC')" },
|
|
160
|
+
buy_token: { type: "string", description: "Token to buy (e.g. 'USDC', 'GRIT')" },
|
|
161
|
+
amount: { type: "string", description: "Amount to sell in human units (e.g. '100')" },
|
|
162
|
+
},
|
|
163
|
+
required: ["sell_token", "buy_token", "amount"],
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
// Oracle
|
|
167
|
+
{
|
|
168
|
+
name: "grinta_update_btc_price",
|
|
169
|
+
description: "Fetch BTC/USD from CoinGecko and push to the OracleRelayer on-chain.",
|
|
170
|
+
inputSchema: { type: "object", properties: {} },
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: "grinta_trigger_update",
|
|
174
|
+
description: "Trigger a manual price/rate update on the GrintaHook.",
|
|
175
|
+
inputSchema: { type: "object", properties: {} },
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
// --- Tool handler ---
|
|
179
|
+
async function handleTool(name, args) {
|
|
180
|
+
switch (name) {
|
|
181
|
+
// --- Reads ---
|
|
182
|
+
case "grinta_get_system_status": {
|
|
183
|
+
const s = await grinta.getSystemStatus();
|
|
184
|
+
const marketPrice = Number(s.marketPrice) / 1e18;
|
|
185
|
+
const redemptionPrice = Number(s.redemptionPrice) / 1e27;
|
|
186
|
+
const RAY_ONE = 10n ** 27n;
|
|
187
|
+
const ratePerSec = Number(s.redemptionRate - RAY_ONE) / 1e27;
|
|
188
|
+
const rateAnnual = ratePerSec * 365.25 * 24 * 3600 * 100;
|
|
189
|
+
return [
|
|
190
|
+
`BTC/USD: $${formatAmount(s.collateralPrice, 18)}`,
|
|
191
|
+
`GRIT Market Price: $${marketPrice.toFixed(6)}`,
|
|
192
|
+
`Redemption Price: $${redemptionPrice.toFixed(6)}`,
|
|
193
|
+
`Redemption Rate: ${rateAnnual >= 0 ? "+" : ""}${rateAnnual.toFixed(6)}% annual`,
|
|
194
|
+
`Total Debt: ${formatAmount(s.totalDebt, 18)} GRIT`,
|
|
195
|
+
`Total Collateral: ${formatAmount(s.totalCollateral, 18)} WBTC (WAD)`,
|
|
196
|
+
`Debt Ceiling: ${formatAmount(s.debtCeiling, 18)} GRIT`,
|
|
197
|
+
`Liquidation Ratio: ${Number(s.liquidationRatio * 100n / WAD)}%`,
|
|
198
|
+
].join("\n");
|
|
199
|
+
}
|
|
200
|
+
case "grinta_get_position_health": {
|
|
201
|
+
const safeId = parseInt(args.safe_id, 10);
|
|
202
|
+
const h = await grinta.getPositionHealth(safeId);
|
|
203
|
+
const ltv = Number(h.ltv * 10000n / WAD) / 100;
|
|
204
|
+
return [
|
|
205
|
+
`SAFE #${safeId}`,
|
|
206
|
+
`Collateral Value: $${formatAmount(h.collateralValue, 18)}`,
|
|
207
|
+
`Debt: ${formatAmount(h.debt, 18)} GRIT`,
|
|
208
|
+
`LTV: ${ltv}%`,
|
|
209
|
+
`Liquidation Price: $${formatAmount(h.liquidationPrice, 18)}`,
|
|
210
|
+
].join("\n");
|
|
211
|
+
}
|
|
212
|
+
case "grinta_get_safe": {
|
|
213
|
+
const safeId = parseInt(args.safe_id, 10);
|
|
214
|
+
const s = await grinta.getSafe(safeId);
|
|
215
|
+
return `SAFE #${safeId}\nCollateral: ${formatAmount(s.collateral, 18)} WBTC (WAD)\nDebt: ${formatAmount(s.debt, 18)} GRIT`;
|
|
216
|
+
}
|
|
217
|
+
case "grinta_get_max_borrow": {
|
|
218
|
+
const safeId = parseInt(args.safe_id, 10);
|
|
219
|
+
const max = await grinta.getMaxBorrow(safeId);
|
|
220
|
+
return `SAFE #${safeId} max borrow: ${formatAmount(max, 18)} GRIT`;
|
|
221
|
+
}
|
|
222
|
+
case "grinta_get_balances": {
|
|
223
|
+
const addr = args.address || CONFIG.ACCOUNT_ADDRESS;
|
|
224
|
+
const eth = new Contract({ abi: ERC20_ABI, address: TOKENS.ETH, providerOrAccount: provider });
|
|
225
|
+
const usdc = new Contract({ abi: ERC20_ABI, address: TOKENS.USDC, providerOrAccount: provider });
|
|
226
|
+
const [ethBal, wbtcBal, gritBal, usdcBal] = await Promise.all([
|
|
227
|
+
eth.balance_of(addr).then((r) => BigInt(r)),
|
|
228
|
+
grinta.getWbtcBalance(addr),
|
|
229
|
+
grinta.getGritBalance(addr),
|
|
230
|
+
usdc.balance_of(addr).then((r) => BigInt(r)),
|
|
231
|
+
]);
|
|
232
|
+
return [
|
|
233
|
+
`Balances for ${addr}`,
|
|
234
|
+
`ETH: ${formatAmount(ethBal, 18)}`,
|
|
235
|
+
`WBTC: ${formatAmount(wbtcBal, 8)}`,
|
|
236
|
+
`GRIT: ${formatAmount(gritBal, 18)}`,
|
|
237
|
+
`USDC: ${formatAmount(usdcBal, 6)}`,
|
|
238
|
+
].join("\n");
|
|
239
|
+
}
|
|
240
|
+
// --- CDP Writes ---
|
|
241
|
+
case "grinta_open_safe": {
|
|
242
|
+
await grinta.openSafe();
|
|
243
|
+
return "SAFE opened successfully.";
|
|
244
|
+
}
|
|
245
|
+
case "grinta_deposit": {
|
|
246
|
+
const safeId = parseInt(args.safe_id, 10);
|
|
247
|
+
const amount = parseAmount(args.amount, 8);
|
|
248
|
+
const tx = await grinta.deposit(safeId, amount);
|
|
249
|
+
return `Deposited ${args.amount} WBTC into SAFE #${safeId}\ntx: ${tx}`;
|
|
250
|
+
}
|
|
251
|
+
case "grinta_withdraw": {
|
|
252
|
+
const safeId = parseInt(args.safe_id, 10);
|
|
253
|
+
const amount = parseAmount(args.amount, 18);
|
|
254
|
+
const tx = await grinta.withdraw(safeId, amount);
|
|
255
|
+
return `Withdrew ${args.amount} WBTC from SAFE #${safeId}\ntx: ${tx}`;
|
|
256
|
+
}
|
|
257
|
+
case "grinta_borrow": {
|
|
258
|
+
const safeId = parseInt(args.safe_id, 10);
|
|
259
|
+
const amount = parseAmount(args.amount, 18);
|
|
260
|
+
const tx = await grinta.borrow(safeId, amount);
|
|
261
|
+
return `Borrowed ${args.amount} GRIT from SAFE #${safeId}\ntx: ${tx}`;
|
|
262
|
+
}
|
|
263
|
+
case "grinta_repay": {
|
|
264
|
+
const safeId = parseInt(args.safe_id, 10);
|
|
265
|
+
const amount = parseAmount(args.amount, 18);
|
|
266
|
+
const tx = await grinta.repay(safeId, amount);
|
|
267
|
+
return `Repaid ${args.amount} GRIT on SAFE #${safeId}\ntx: ${tx}`;
|
|
268
|
+
}
|
|
269
|
+
case "grinta_close_safe": {
|
|
270
|
+
const safeId = parseInt(args.safe_id, 10);
|
|
271
|
+
const tx = await grinta.closeSafe(safeId);
|
|
272
|
+
return `Closed SAFE #${safeId}\ntx: ${tx}`;
|
|
273
|
+
}
|
|
274
|
+
// --- DEX ---
|
|
275
|
+
case "grinta_swap": {
|
|
276
|
+
const { txHash, sellName, buyName } = await ekuboSwap(account, provider, args.sell_token, args.buy_token, args.amount);
|
|
277
|
+
return `Swapped ${args.amount} ${sellName} → ${buyName}\ntx: ${txHash}`;
|
|
278
|
+
}
|
|
279
|
+
// --- Oracle ---
|
|
280
|
+
case "grinta_update_btc_price": {
|
|
281
|
+
const { txHash, priceUsd } = await updateBtcPrice(account);
|
|
282
|
+
return `Updated BTC/USD to $${priceUsd}\ntx: ${txHash}`;
|
|
283
|
+
}
|
|
284
|
+
case "grinta_trigger_update": {
|
|
285
|
+
const tx = await grinta.triggerUpdate();
|
|
286
|
+
return `Triggered GrintaHook update\ntx: ${tx}`;
|
|
287
|
+
}
|
|
288
|
+
default:
|
|
289
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// --- MCP Server ---
|
|
293
|
+
const server = new Server({ name: "grinta-cdp", version: "0.2.0" }, { capabilities: { tools: {} } });
|
|
294
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
295
|
+
return { tools: TOOLS };
|
|
296
|
+
});
|
|
297
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
298
|
+
const { name, arguments: args } = request.params;
|
|
299
|
+
try {
|
|
300
|
+
const result = await handleTool(name, (args ?? {}));
|
|
301
|
+
return { content: [{ type: "text", text: result }] };
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
return {
|
|
305
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
306
|
+
isError: true,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
async function main() {
|
|
311
|
+
if (!CONFIG.ACCOUNT_ADDRESS || !CONFIG.PRIVATE_KEY) {
|
|
312
|
+
console.error("Missing STARKNET_ACCOUNT_ADDRESS or STARKNET_PRIVATE_KEY in .env");
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
const transport = new StdioServerTransport();
|
|
316
|
+
await server.connect(transport);
|
|
317
|
+
console.error(`Grinta MCP server running (dev mode) — account: ${CONFIG.ACCOUNT_ADDRESS}`);
|
|
318
|
+
}
|
|
319
|
+
main().catch((e) => {
|
|
320
|
+
console.error("Fatal:", e);
|
|
321
|
+
process.exit(1);
|
|
322
|
+
});
|