@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.
@@ -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
@@ -0,0 +1,8 @@
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
+ export {};
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
+ });