kairu-mcp 0.0.1
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/dist/index.js +232 -0
- package/package.json +28 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Kairu MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Exposes Kairu's Aave position data as MCP tools for Claude Desktop,
|
|
6
|
+
* claude.ai (Pro/Team with MCP integrations), and any other MCP client.
|
|
7
|
+
*
|
|
8
|
+
* Environment variables:
|
|
9
|
+
* KAIRU_API_BASE_URL API base URL (default: https://api.kairu.app)
|
|
10
|
+
* KAIRU_API_TOKEN Bearer token — required for authenticated tools (get_groups)
|
|
11
|
+
* KAIRU_DEFAULT_ADDRESS Wallet address to use when none is supplied in the tool call
|
|
12
|
+
*
|
|
13
|
+
* Claude Desktop setup (~/.claude/settings.json or claude_desktop_config.json):
|
|
14
|
+
* {
|
|
15
|
+
* "mcpServers": {
|
|
16
|
+
* "kairu": {
|
|
17
|
+
* "command": "node",
|
|
18
|
+
* "args": ["/absolute/path/to/packages/mcp/dist/index.js"],
|
|
19
|
+
* "env": {
|
|
20
|
+
* "KAIRU_API_TOKEN": "<your-bearer-token>",
|
|
21
|
+
* "KAIRU_DEFAULT_ADDRESS": "0xYourWalletAddress"
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* Build first: pnpm --filter @defi-assistant/mcp build
|
|
28
|
+
*/
|
|
29
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
30
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
31
|
+
import { z } from 'zod';
|
|
32
|
+
// ─── Config ──────────────────────────────────────────────────────────────────
|
|
33
|
+
const BASE_URL = process.env['KAIRU_API_BASE_URL'] ?? 'https://api.kairu.app';
|
|
34
|
+
const TOKEN = process.env['KAIRU_API_TOKEN'];
|
|
35
|
+
const DEFAULT_ADDRESS = process.env['KAIRU_DEFAULT_ADDRESS'];
|
|
36
|
+
// ─── HTTP helper ─────────────────────────────────────────────────────────────
|
|
37
|
+
async function apiFetch(path, options) {
|
|
38
|
+
const headers = {
|
|
39
|
+
'Content-Type': 'application/json',
|
|
40
|
+
...(TOKEN ? { Authorization: `Bearer ${TOKEN}` } : {}),
|
|
41
|
+
};
|
|
42
|
+
const res = await fetch(`${BASE_URL}${path}`, {
|
|
43
|
+
...options,
|
|
44
|
+
headers: { ...headers, ...options?.headers },
|
|
45
|
+
});
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
const body = await res.text().catch(() => '');
|
|
48
|
+
throw new Error(`Kairu API ${res.status}: ${body}`);
|
|
49
|
+
}
|
|
50
|
+
return res.json();
|
|
51
|
+
}
|
|
52
|
+
function toText(data) {
|
|
53
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
54
|
+
}
|
|
55
|
+
function resolveAddress(address) {
|
|
56
|
+
const resolved = address ?? DEFAULT_ADDRESS;
|
|
57
|
+
if (!resolved)
|
|
58
|
+
throw new Error('No wallet address provided. Pass `address` or set KAIRU_DEFAULT_ADDRESS.');
|
|
59
|
+
return resolved;
|
|
60
|
+
}
|
|
61
|
+
// ─── Chain / protocol shared schema ──────────────────────────────────────────
|
|
62
|
+
const chainId = z
|
|
63
|
+
.number()
|
|
64
|
+
.optional()
|
|
65
|
+
.describe('Chain ID: 1=Ethereum (default), 137=Polygon, 42161=Arbitrum, 10=Optimism, 8453=Base');
|
|
66
|
+
const protocol = z.string().optional().describe('Protocol to query (default: aave)');
|
|
67
|
+
const address = z
|
|
68
|
+
.string()
|
|
69
|
+
.optional()
|
|
70
|
+
.describe(`Wallet address (0x...). Defaults to KAIRU_DEFAULT_ADDRESS env var${DEFAULT_ADDRESS ? ` (${DEFAULT_ADDRESS.slice(0, 6)}…)` : ''}.`);
|
|
71
|
+
function chainParams(chainId, protocol) {
|
|
72
|
+
const p = new URLSearchParams();
|
|
73
|
+
if (chainId)
|
|
74
|
+
p.set('chainId', String(chainId));
|
|
75
|
+
if (protocol)
|
|
76
|
+
p.set('protocol', protocol);
|
|
77
|
+
return p;
|
|
78
|
+
}
|
|
79
|
+
// ─── Server ───────────────────────────────────────────────────────────────────
|
|
80
|
+
const server = new McpServer({ name: 'kairu', version: '0.0.1' });
|
|
81
|
+
// ─── Tool: get_position ───────────────────────────────────────────────────────
|
|
82
|
+
server.registerTool('get_position', {
|
|
83
|
+
description: 'Fetch current Aave position: health factor, collateral, debt, APYs, liquidation prices, safe borrow/withdraw limits, risk level, and yield analysis.',
|
|
84
|
+
inputSchema: z.object({ address, chainId, protocol }),
|
|
85
|
+
}, async (args) => {
|
|
86
|
+
const addr = resolveAddress(args.address);
|
|
87
|
+
const params = chainParams(args.chainId, args.protocol);
|
|
88
|
+
const data = await apiFetch(`/api/positions/${addr}?${params}`);
|
|
89
|
+
return toText(data);
|
|
90
|
+
});
|
|
91
|
+
// ─── Tool: simulate ───────────────────────────────────────────────────────────
|
|
92
|
+
server.registerTool('simulate', {
|
|
93
|
+
description: 'Run a what-if simulation: price change, deposit, withdraw, borrow, repay, or repay-with-collateral. Returns the new health factor and risk assessment.',
|
|
94
|
+
inputSchema: z.object({
|
|
95
|
+
address,
|
|
96
|
+
chainId,
|
|
97
|
+
protocol,
|
|
98
|
+
action: z
|
|
99
|
+
.enum(['priceChange', 'deposit', 'withdraw', 'borrow', 'repay', 'repayWithCollateral'])
|
|
100
|
+
.describe('Type of action to simulate'),
|
|
101
|
+
asset: z.string().describe('Asset symbol (ETH, USDC, WBTC…)'),
|
|
102
|
+
amount: z
|
|
103
|
+
.number()
|
|
104
|
+
.optional()
|
|
105
|
+
.describe('Amount for deposit/withdraw/borrow/repay/repayWithCollateral actions'),
|
|
106
|
+
percentChange: z
|
|
107
|
+
.number()
|
|
108
|
+
.optional()
|
|
109
|
+
.describe('Percentage change for priceChange action (e.g. -30 for a 30% drop)'),
|
|
110
|
+
collateralAsset: z
|
|
111
|
+
.string()
|
|
112
|
+
.optional()
|
|
113
|
+
.describe('Required for repayWithCollateral: the collateral asset to sell'),
|
|
114
|
+
}),
|
|
115
|
+
}, async (args) => {
|
|
116
|
+
const addr = resolveAddress(args.address);
|
|
117
|
+
const data = await apiFetch('/api/positions/simulate', {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
body: JSON.stringify({
|
|
120
|
+
address: addr,
|
|
121
|
+
chainId: args.chainId ?? 1,
|
|
122
|
+
protocol: args.protocol ?? 'aave',
|
|
123
|
+
type: args.action,
|
|
124
|
+
asset: args.asset,
|
|
125
|
+
...(args.amount !== undefined && { amount: args.amount }),
|
|
126
|
+
...(args.percentChange !== undefined && { percentChange: args.percentChange }),
|
|
127
|
+
...(args.collateralAsset !== undefined && { collateralAsset: args.collateralAsset }),
|
|
128
|
+
}),
|
|
129
|
+
});
|
|
130
|
+
return toText(data);
|
|
131
|
+
});
|
|
132
|
+
// ─── Tool: get_market_data ────────────────────────────────────────────────────
|
|
133
|
+
server.registerTool('get_market_data', {
|
|
134
|
+
description: 'Get Aave market data: supply/borrow APYs, available liquidity, and caps for all reserves (or a specific asset).',
|
|
135
|
+
inputSchema: z.object({
|
|
136
|
+
asset: z
|
|
137
|
+
.string()
|
|
138
|
+
.optional()
|
|
139
|
+
.describe('Filter to a specific asset symbol (e.g. USDC, ETH). Omit for all reserves.'),
|
|
140
|
+
chainId,
|
|
141
|
+
protocol,
|
|
142
|
+
}),
|
|
143
|
+
}, async (args) => {
|
|
144
|
+
const params = chainParams(args.chainId, args.protocol);
|
|
145
|
+
if (args.asset)
|
|
146
|
+
params.set('asset', args.asset);
|
|
147
|
+
const data = await apiFetch(`/api/market/assets?${params}`);
|
|
148
|
+
return toText(data);
|
|
149
|
+
});
|
|
150
|
+
// ─── Tool: get_apy_history ────────────────────────────────────────────────────
|
|
151
|
+
server.registerTool('get_apy_history', {
|
|
152
|
+
description: 'Get historical supply and borrow APY trends for an asset over a given timeframe.',
|
|
153
|
+
inputSchema: z.object({
|
|
154
|
+
assetAddress: z.string().describe('Token contract address'),
|
|
155
|
+
timeframe: z
|
|
156
|
+
.enum(['1d', '7d', '30d', '90d', '1y'])
|
|
157
|
+
.optional()
|
|
158
|
+
.describe('History window (default: 7d)'),
|
|
159
|
+
chainId,
|
|
160
|
+
protocol,
|
|
161
|
+
}),
|
|
162
|
+
}, async (args) => {
|
|
163
|
+
const params = chainParams(args.chainId, args.protocol);
|
|
164
|
+
if (args.timeframe)
|
|
165
|
+
params.set('timeframe', args.timeframe);
|
|
166
|
+
const data = await apiFetch(`/api/market/history/${args.assetAddress}?${params}`);
|
|
167
|
+
return toText(data);
|
|
168
|
+
});
|
|
169
|
+
// ─── Tool: get_reserve_details ────────────────────────────────────────────────
|
|
170
|
+
server.registerTool('get_reserve_details', {
|
|
171
|
+
description: 'Get detailed reserve parameters for an asset: caps, utilization rate, liquidation thresholds, E-mode categories.',
|
|
172
|
+
inputSchema: z.object({
|
|
173
|
+
assetAddress: z.string().describe('Token contract address'),
|
|
174
|
+
chainId,
|
|
175
|
+
protocol,
|
|
176
|
+
}),
|
|
177
|
+
}, async (args) => {
|
|
178
|
+
const params = chainParams(args.chainId, args.protocol);
|
|
179
|
+
const data = await apiFetch(`/api/market/reserve/${args.assetAddress}?${params}`);
|
|
180
|
+
return toText(data);
|
|
181
|
+
});
|
|
182
|
+
// ─── Tool: get_rewards ────────────────────────────────────────────────────────
|
|
183
|
+
server.registerTool('get_rewards', {
|
|
184
|
+
description: "Get user's claimable Merit rewards from Aave's reward program with USD values.",
|
|
185
|
+
inputSchema: z.object({ address, chainId, protocol }),
|
|
186
|
+
}, async (args) => {
|
|
187
|
+
const addr = resolveAddress(args.address);
|
|
188
|
+
const params = chainParams(args.chainId, args.protocol);
|
|
189
|
+
const data = await apiFetch(`/api/positions/${addr}/rewards?${params}`);
|
|
190
|
+
return toText(data);
|
|
191
|
+
});
|
|
192
|
+
// ─── Tool: get_gho_position ───────────────────────────────────────────────────
|
|
193
|
+
server.registerTool('get_gho_position', {
|
|
194
|
+
description: "Get user's GHO and sGHO balances with earnings.",
|
|
195
|
+
inputSchema: z.object({ address, chainId, protocol }),
|
|
196
|
+
}, async (args) => {
|
|
197
|
+
const addr = resolveAddress(args.address);
|
|
198
|
+
const params = chainParams(args.chainId, args.protocol);
|
|
199
|
+
const data = await apiFetch(`/api/positions/${addr}/gho?${params}`);
|
|
200
|
+
return toText(data);
|
|
201
|
+
});
|
|
202
|
+
// ─── Tool: get_transaction_history ────────────────────────────────────────────
|
|
203
|
+
server.registerTool('get_transaction_history', {
|
|
204
|
+
description: "Get user's past Aave transactions (supplies, borrows, repays, withdrawals).",
|
|
205
|
+
inputSchema: z.object({
|
|
206
|
+
address,
|
|
207
|
+
limit: z.number().optional().describe('Number of transactions to return (default: 20)'),
|
|
208
|
+
chainId,
|
|
209
|
+
protocol,
|
|
210
|
+
}),
|
|
211
|
+
}, async (args) => {
|
|
212
|
+
const addr = resolveAddress(args.address);
|
|
213
|
+
const params = chainParams(args.chainId, args.protocol);
|
|
214
|
+
if (args.limit)
|
|
215
|
+
params.set('limit', String(args.limit));
|
|
216
|
+
const data = await apiFetch(`/api/positions/${addr}/transactions?${params}`);
|
|
217
|
+
return toText(data);
|
|
218
|
+
});
|
|
219
|
+
// ─── Tool: get_groups (authenticated only) ────────────────────────────────────
|
|
220
|
+
if (TOKEN) {
|
|
221
|
+
server.registerTool('get_groups', {
|
|
222
|
+
description: 'List all wallet groups for the authenticated user, including nested wallet addresses and labels.',
|
|
223
|
+
inputSchema: z.object({}),
|
|
224
|
+
}, async () => {
|
|
225
|
+
const data = await apiFetch('/api/groups');
|
|
226
|
+
return toText(data);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
// ─── Start ────────────────────────────────────────────────────────────────────
|
|
230
|
+
const transport = new StdioServerTransport();
|
|
231
|
+
await server.connect(transport);
|
|
232
|
+
console.error(`Kairu MCP server started (${BASE_URL})`);
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kairu-mcp",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "MCP server for Kairu — exposes Aave position data to Claude and other MCP clients",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"kairu-mcp": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc && echo '#!/usr/bin/env node' | cat - dist/index.js > dist/index.tmp.js && mv dist/index.tmp.js dist/index.js && chmod +x dist/index.js",
|
|
11
|
+
"start": "node dist/index.js",
|
|
12
|
+
"dev": "tsx src/index.ts"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
22
|
+
"zod": "^3.25.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"tsx": "^4.19.0",
|
|
26
|
+
"typescript": "^5.3.0"
|
|
27
|
+
}
|
|
28
|
+
}
|