@earnforge/mcp 0.1.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,8 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+
3
+ //#region src/index.d.ts
4
+ declare function createServer(): McpServer;
5
+ declare function main(): Promise<void>;
6
+ //#endregion
7
+ export { createServer, main };
8
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/index.ts"],"mappings":";;;iBAiCgB,YAAA,CAAA,GAAgB,SAAA;AAAA,iBA0WV,IAAA,CAAA,GAAQ,OAAA"}
@@ -0,0 +1,316 @@
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 { z } from "zod";
5
+ import { createEarnForge } from "@earnforge/sdk";
6
+ //#region src/index.ts
7
+ function json(data) {
8
+ return { content: [{
9
+ type: "text",
10
+ text: JSON.stringify(data, null, 2)
11
+ }] };
12
+ }
13
+ function error(message) {
14
+ return {
15
+ content: [{
16
+ type: "text",
17
+ text: message
18
+ }],
19
+ isError: true
20
+ };
21
+ }
22
+ function buildForge() {
23
+ return createEarnForge({
24
+ composerApiKey: process.env.LIFI_API_KEY,
25
+ cache: {
26
+ ttl: 6e4,
27
+ maxSize: 200
28
+ }
29
+ });
30
+ }
31
+ function createServer() {
32
+ const server = new McpServer({
33
+ name: "earnforge-mcp",
34
+ version: "0.1.0"
35
+ });
36
+ const forge = buildForge();
37
+ server.tool("get-earn-vaults", "List LI.FI Earn vaults with optional filters. Returns paginated vault data including APY, TVL, protocol info, and tags. Use chainId (number, not chain name), asset symbol, minTvl, sortBy, limit, and strategy preset to narrow results.", {
38
+ chainId: z.number().optional().describe("EVM chain ID (e.g. 8453 for Base, 1 for Ethereum). Must be a number, not a chain name."),
39
+ asset: z.string().optional().describe("Filter by underlying token symbol (e.g. \"USDC\", \"ETH\")."),
40
+ minTvl: z.number().optional().describe("Minimum TVL in USD. Vaults below this are excluded."),
41
+ sortBy: z.string().optional().describe("Sort field (e.g. \"apy\", \"tvl\")."),
42
+ limit: z.number().optional().describe("Maximum number of vaults to return (default 10)."),
43
+ strategy: z.enum([
44
+ "conservative",
45
+ "max-apy",
46
+ "diversified",
47
+ "risk-adjusted"
48
+ ]).optional().describe("Strategy preset filter: \"conservative\" (stablecoins, blue-chip, TVL>$50M), \"max-apy\" (highest APY), \"diversified\" (multi-chain spread), \"risk-adjusted\" (risk score >= 7).")
49
+ }, async (params) => {
50
+ try {
51
+ const limit = params.limit ?? 10;
52
+ const results = (await forge.vaults.top({
53
+ chainId: params.chainId,
54
+ asset: params.asset,
55
+ minTvl: params.minTvl,
56
+ limit,
57
+ strategy: params.strategy
58
+ })).map(summarizeVault);
59
+ return json({
60
+ count: results.length,
61
+ vaults: results
62
+ });
63
+ } catch (err) {
64
+ return error(`Failed to list vaults: ${err.message}`);
65
+ }
66
+ });
67
+ server.tool("get-earn-vault", "Get a single LI.FI Earn vault by its slug (format: \"<chainId>-<address>\", e.g. \"8453-0xbeef...\"). Returns full vault details including APY breakdown, TVL, underlying tokens, protocol, deposit/redeem packs, and tags.", { slug: z.string().describe("Vault slug in the format \"<chainId>-<vaultAddress>\" (e.g. \"8453-0xbeef0e0834849acc03f0089f01f4f1eeb06873c9\").") }, async (params) => {
68
+ try {
69
+ return json(await forge.vaults.get(params.slug));
70
+ } catch (err) {
71
+ return error(`Failed to get vault: ${err.message}`);
72
+ }
73
+ });
74
+ server.tool("get-earn-chains", "List all blockchain chains supported by LI.FI Earn. Returns chain IDs, names, and CAIP identifiers. Use the chainId from the results to filter vaults.", {}, async () => {
75
+ try {
76
+ const chains = await forge.chains.list();
77
+ return json({
78
+ count: chains.length,
79
+ chains
80
+ });
81
+ } catch (err) {
82
+ return error(`Failed to list chains: ${err.message}`);
83
+ }
84
+ });
85
+ server.tool("get-earn-protocols", "List all DeFi protocols available on LI.FI Earn. Returns protocol names and URLs. Protocol names can be used to understand which vaults belong to which protocol.", {}, async () => {
86
+ try {
87
+ const protocols = await forge.protocols.list();
88
+ return json({
89
+ count: protocols.length,
90
+ protocols
91
+ });
92
+ } catch (err) {
93
+ return error(`Failed to list protocols: ${err.message}`);
94
+ }
95
+ });
96
+ server.tool("get-earn-portfolio", "Get DeFi portfolio positions for a wallet address. Returns all earn positions including chain, protocol, asset, USD balance, and native balance.", { wallet: z.string().describe("Wallet address (0x...) to look up portfolio positions for.") }, async (params) => {
97
+ try {
98
+ return json(await forge.portfolio.get(params.wallet));
99
+ } catch (err) {
100
+ return error(`Failed to get portfolio: ${err.message}`);
101
+ }
102
+ });
103
+ server.tool("get-vault-risk", "Compute a composite 0-10 risk score for a vault. Score dimensions: TVL magnitude, APY stability, protocol maturity, redeemability, and asset type. Labels: \"low\" (>=7), \"medium\" (>=4), \"high\" (<4). Higher score = safer.", { slug: z.string().describe("Vault slug in the format \"<chainId>-<vaultAddress>\".") }, async (params) => {
104
+ try {
105
+ const vault = await forge.vaults.get(params.slug);
106
+ const risk = forge.riskScore(vault);
107
+ return json({
108
+ slug: params.slug,
109
+ name: vault.name,
110
+ ...risk
111
+ });
112
+ } catch (err) {
113
+ return error(`Failed to compute risk score: ${err.message}`);
114
+ }
115
+ });
116
+ server.tool("quote-vault-deposit", "Build a deposit quote for an Earn vault. Requires a LI.FI API key (set LIFI_API_KEY env var). Returns the quote with transaction data ready to sign. IMPORTANT: Before executing the deposit, use check-allowance with the quote's approvalAddress to verify token approval.", {
117
+ slug: z.string().describe("Vault slug in the format \"<chainId>-<vaultAddress>\"."),
118
+ wallet: z.string().describe("Wallet address (0x...) that will execute the deposit."),
119
+ fromAmount: z.string().describe("Human-readable amount to deposit (e.g. \"100\" for 100 USDC)."),
120
+ fromToken: z.string().optional().describe("Override the from-token address. Defaults to the vault's first underlying token."),
121
+ fromChain: z.number().optional().describe("Override the source chain ID. Defaults to the vault's chain."),
122
+ slippage: z.number().optional().describe("Slippage tolerance (e.g. 0.03 for 3%). Defaults to API default.")
123
+ }, async (params) => {
124
+ try {
125
+ const vault = await forge.vaults.get(params.slug);
126
+ const result = await forge.buildDepositQuote(vault, {
127
+ fromAmount: params.fromAmount,
128
+ wallet: params.wallet,
129
+ fromToken: params.fromToken,
130
+ fromChain: params.fromChain,
131
+ slippage: params.slippage
132
+ });
133
+ return json({
134
+ vault: result.vault.name,
135
+ humanAmount: result.humanAmount,
136
+ rawAmount: result.rawAmount,
137
+ decimals: result.decimals,
138
+ estimate: {
139
+ tool: result.quote.estimate.tool,
140
+ toAmount: result.quote.estimate.toAmount,
141
+ toAmountMin: result.quote.estimate.toAmountMin,
142
+ executionDuration: result.quote.estimate.executionDuration,
143
+ gasCosts: result.quote.estimate.gasCosts,
144
+ feeCosts: result.quote.estimate.feeCosts
145
+ },
146
+ transactionRequest: {
147
+ to: result.quote.transactionRequest.to,
148
+ value: result.quote.transactionRequest.value,
149
+ chainId: result.quote.transactionRequest.chainId
150
+ }
151
+ });
152
+ } catch (err) {
153
+ return error(`Failed to build deposit quote: ${err.message}`);
154
+ }
155
+ });
156
+ server.tool("quote-vault-redeem", "Build a withdrawal/redeem quote for an Earn vault. Withdraws vault share tokens back to the underlying asset. Requires LIFI_API_KEY. Checks isRedeemable before quoting.", {
157
+ slug: z.string().describe("Vault slug \"<chainId>-<vaultAddress>\"."),
158
+ wallet: z.string().describe("Wallet address (0x...)."),
159
+ fromAmount: z.string().describe("Amount of vault share tokens to redeem (human-readable)."),
160
+ toToken: z.string().optional().describe("Override destination token address."),
161
+ toChain: z.number().optional().describe("Override destination chain ID."),
162
+ slippage: z.number().optional().describe("Slippage tolerance.")
163
+ }, async (params) => {
164
+ try {
165
+ const vault = await forge.vaults.get(params.slug);
166
+ const result = await forge.buildRedeemQuote(vault, {
167
+ fromAmount: params.fromAmount,
168
+ wallet: params.wallet,
169
+ toToken: params.toToken,
170
+ toChain: params.toChain,
171
+ slippage: params.slippage
172
+ });
173
+ return json({
174
+ vault: result.vault.name,
175
+ humanAmount: result.humanAmount,
176
+ rawAmount: result.rawAmount,
177
+ isRedeemable: vault.isRedeemable,
178
+ estimate: {
179
+ toAmount: result.quote.estimate.toAmount,
180
+ toAmountMin: result.quote.estimate.toAmountMin,
181
+ executionDuration: result.quote.estimate.executionDuration
182
+ },
183
+ transactionRequest: {
184
+ to: result.quote.transactionRequest.to,
185
+ value: result.quote.transactionRequest.value,
186
+ chainId: result.quote.transactionRequest.chainId
187
+ }
188
+ });
189
+ } catch (err) {
190
+ return error(`Failed to build redeem quote: ${err.message}`);
191
+ }
192
+ });
193
+ server.tool("check-allowance", "Check ERC-20 token allowance and build an approval tx if needed. Use before depositing — the Composer contract needs token approval. Get the spender from quote.estimate.approvalAddress.", {
194
+ rpcUrl: z.string().describe("JSON-RPC endpoint for the chain."),
195
+ tokenAddress: z.string().describe("ERC-20 token contract address."),
196
+ owner: z.string().describe("Wallet address (token holder)."),
197
+ spender: z.string().describe("Spender address (from quote.estimate.approvalAddress)."),
198
+ requiredAmount: z.string().describe("Required amount in smallest unit (from quote rawAmount)."),
199
+ chainId: z.number().describe("Chain ID for the approval transaction.")
200
+ }, async (params) => {
201
+ try {
202
+ const { checkAllowance, buildApprovalTx, MAX_UINT256 } = await import("@earnforge/sdk");
203
+ const required = BigInt(params.requiredAmount);
204
+ const result = await checkAllowance(params.rpcUrl, params.tokenAddress, params.owner, params.spender, required);
205
+ const response = {
206
+ allowance: result.allowance.toString(),
207
+ sufficient: result.sufficient,
208
+ requiredAmount: result.requiredAmount.toString()
209
+ };
210
+ if (!result.sufficient) {
211
+ response.approvalTx = buildApprovalTx(params.tokenAddress, params.spender, MAX_UINT256, params.chainId);
212
+ response.note = "Allowance insufficient. Sign the approvalTx before depositing.";
213
+ }
214
+ return json(response);
215
+ } catch (err) {
216
+ return error(`Failed to check allowance: ${err.message}`);
217
+ }
218
+ });
219
+ server.tool("suggest-allocation", "Get a portfolio allocation suggestion. Uses a risk-adjusted scoring engine to recommend how to split funds across multiple vaults. Returns allocation percentages, expected APY, and risk scores per vault.", {
220
+ amount: z.number().describe("Total USD amount to allocate across vaults."),
221
+ asset: z.string().optional().describe("Filter vaults by underlying token symbol (e.g. \"USDC\")."),
222
+ maxChains: z.number().optional().describe("Maximum number of different chains to spread across (default 5)."),
223
+ maxVaults: z.number().optional().describe("Maximum number of vaults in the allocation (default 5)."),
224
+ strategy: z.enum([
225
+ "conservative",
226
+ "max-apy",
227
+ "diversified",
228
+ "risk-adjusted"
229
+ ]).optional().describe("Strategy preset to guide allocation.")
230
+ }, async (params) => {
231
+ try {
232
+ const result = await forge.suggest({
233
+ amount: params.amount,
234
+ asset: params.asset,
235
+ maxChains: params.maxChains,
236
+ maxVaults: params.maxVaults,
237
+ strategy: params.strategy
238
+ });
239
+ return json({
240
+ totalAmount: result.totalAmount,
241
+ expectedApy: result.expectedApy,
242
+ allocations: result.allocations.map((a) => ({
243
+ vault: a.vault.name,
244
+ slug: a.vault.slug,
245
+ chainId: a.vault.chainId,
246
+ protocol: a.vault.protocol.name,
247
+ percentage: a.percentage,
248
+ amount: a.amount,
249
+ apy: a.apy,
250
+ riskScore: a.risk.score,
251
+ riskLabel: a.risk.label
252
+ }))
253
+ });
254
+ } catch (err) {
255
+ return error(`Failed to generate allocation suggestion: ${err.message}`);
256
+ }
257
+ });
258
+ server.tool("run-doctor", "Run preflight / pitfall checks on a vault before depositing. Checks: isTransactional, chain mismatch, gas token balance, token balance sufficiency, underlyingTokens existence, and redeemability. Returns a pass/fail report with detailed issues.", {
259
+ slug: z.string().describe("Vault slug in the format \"<chainId>-<vaultAddress>\"."),
260
+ wallet: z.string().describe("Wallet address (0x...) to run checks against."),
261
+ walletChainId: z.number().optional().describe("Chain ID the wallet is currently connected to. Used to detect chain mismatch."),
262
+ depositAmount: z.string().optional().describe("Human-readable amount to deposit. Used for balance sufficiency check.")
263
+ }, async (params) => {
264
+ try {
265
+ const vault = await forge.vaults.get(params.slug);
266
+ const report = forge.preflight(vault, params.wallet, {
267
+ walletChainId: params.walletChainId,
268
+ depositAmount: params.depositAmount
269
+ });
270
+ return json({
271
+ ok: report.ok,
272
+ vault: report.vault.name,
273
+ slug: report.vault.slug,
274
+ wallet: report.wallet,
275
+ issueCount: report.issues.length,
276
+ issues: report.issues
277
+ });
278
+ } catch (err) {
279
+ return error(`Failed to run doctor: ${err.message}`);
280
+ }
281
+ });
282
+ return server;
283
+ }
284
+ function summarizeVault(vault) {
285
+ return {
286
+ name: vault.name,
287
+ slug: vault.slug,
288
+ chainId: vault.chainId,
289
+ network: vault.network,
290
+ protocol: vault.protocol.name,
291
+ tags: vault.tags,
292
+ apy: {
293
+ total: vault.analytics.apy.total,
294
+ base: vault.analytics.apy.base,
295
+ reward: vault.analytics.apy.reward
296
+ },
297
+ tvlUsd: vault.analytics.tvl.usd,
298
+ underlyingTokens: vault.underlyingTokens.map((t) => t.symbol),
299
+ isTransactional: vault.isTransactional,
300
+ isRedeemable: vault.isRedeemable
301
+ };
302
+ }
303
+ async function main() {
304
+ const server = createServer();
305
+ const transport = new StdioServerTransport();
306
+ await server.connect(transport);
307
+ }
308
+ const entrypoint = process.argv[1] ?? "";
309
+ if (import.meta.url === `file://${entrypoint}` || entrypoint.endsWith("earnforge-mcp")) main().catch((err) => {
310
+ console.error("EarnForge MCP server failed to start:", err);
311
+ process.exit(1);
312
+ });
313
+ //#endregion
314
+ export { createServer, main };
315
+
316
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\n// SPDX-License-Identifier: Apache-2.0\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { z } from 'zod';\nimport {\n createEarnForge,\n type Vault,\n type StrategyPreset,\n} from '@earnforge/sdk';\n\n// ── Helpers ─────────────────────────────────────────────────────────\n\nfunction json(data: unknown): { content: Array<{ type: 'text'; text: string }> } {\n return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }] };\n}\n\nfunction error(message: string): { content: Array<{ type: 'text'; text: string }>; isError: true } {\n return { content: [{ type: 'text' as const, text: message }], isError: true };\n}\n\n// ── Forge instance ──────────────────────────────────────────────────\n\nfunction buildForge() {\n return createEarnForge({\n composerApiKey: process.env.LIFI_API_KEY,\n cache: { ttl: 60_000, maxSize: 200 },\n });\n}\n\n// ── Server ──────────────────────────────────────────────────────────\n\nexport function createServer(): McpServer {\n const server = new McpServer({\n name: 'earnforge-mcp',\n version: '0.1.0',\n });\n\n const forge = buildForge();\n\n // ── get-earn-vaults ───────────────────────────────────────────────\n\n server.tool(\n 'get-earn-vaults',\n 'List LI.FI Earn vaults with optional filters. Returns paginated vault data including APY, TVL, protocol info, and tags. Use chainId (number, not chain name), asset symbol, minTvl, sortBy, limit, and strategy preset to narrow results.',\n {\n chainId: z.number().optional().describe('EVM chain ID (e.g. 8453 for Base, 1 for Ethereum). Must be a number, not a chain name.'),\n asset: z.string().optional().describe('Filter by underlying token symbol (e.g. \"USDC\", \"ETH\").'),\n minTvl: z.number().optional().describe('Minimum TVL in USD. Vaults below this are excluded.'),\n sortBy: z.string().optional().describe('Sort field (e.g. \"apy\", \"tvl\").'),\n limit: z.number().optional().describe('Maximum number of vaults to return (default 10).'),\n strategy: z.enum(['conservative', 'max-apy', 'diversified', 'risk-adjusted']).optional()\n .describe('Strategy preset filter: \"conservative\" (stablecoins, blue-chip, TVL>$50M), \"max-apy\" (highest APY), \"diversified\" (multi-chain spread), \"risk-adjusted\" (risk score >= 7).'),\n },\n async (params) => {\n try {\n const limit = params.limit ?? 10;\n const vaults = await forge.vaults.top({\n chainId: params.chainId,\n asset: params.asset,\n minTvl: params.minTvl,\n limit,\n strategy: params.strategy as StrategyPreset | undefined,\n });\n\n const results = vaults.map(summarizeVault);\n return json({ count: results.length, vaults: results });\n } catch (err) {\n return error(`Failed to list vaults: ${(err as Error).message}`);\n }\n },\n );\n\n // ── get-earn-vault ────────────────────────────────────────────────\n\n server.tool(\n 'get-earn-vault',\n 'Get a single LI.FI Earn vault by its slug (format: \"<chainId>-<address>\", e.g. \"8453-0xbeef...\"). Returns full vault details including APY breakdown, TVL, underlying tokens, protocol, deposit/redeem packs, and tags.',\n {\n slug: z.string().describe('Vault slug in the format \"<chainId>-<vaultAddress>\" (e.g. \"8453-0xbeef0e0834849acc03f0089f01f4f1eeb06873c9\").'),\n },\n async (params) => {\n try {\n const vault = await forge.vaults.get(params.slug);\n return json(vault);\n } catch (err) {\n return error(`Failed to get vault: ${(err as Error).message}`);\n }\n },\n );\n\n // ── get-earn-chains ───────────────────────────────────────────────\n\n server.tool(\n 'get-earn-chains',\n 'List all blockchain chains supported by LI.FI Earn. Returns chain IDs, names, and CAIP identifiers. Use the chainId from the results to filter vaults.',\n {},\n async () => {\n try {\n const chains = await forge.chains.list();\n return json({ count: chains.length, chains });\n } catch (err) {\n return error(`Failed to list chains: ${(err as Error).message}`);\n }\n },\n );\n\n // ── get-earn-protocols ────────────────────────────────────────────\n\n server.tool(\n 'get-earn-protocols',\n 'List all DeFi protocols available on LI.FI Earn. Returns protocol names and URLs. Protocol names can be used to understand which vaults belong to which protocol.',\n {},\n async () => {\n try {\n const protocols = await forge.protocols.list();\n return json({ count: protocols.length, protocols });\n } catch (err) {\n return error(`Failed to list protocols: ${(err as Error).message}`);\n }\n },\n );\n\n // ── get-earn-portfolio ────────────────────────────────────────────\n\n server.tool(\n 'get-earn-portfolio',\n 'Get DeFi portfolio positions for a wallet address. Returns all earn positions including chain, protocol, asset, USD balance, and native balance.',\n {\n wallet: z.string().describe('Wallet address (0x...) to look up portfolio positions for.'),\n },\n async (params) => {\n try {\n const portfolio = await forge.portfolio.get(params.wallet);\n return json(portfolio);\n } catch (err) {\n return error(`Failed to get portfolio: ${(err as Error).message}`);\n }\n },\n );\n\n // ── get-vault-risk ────────────────────────────────────────────────\n\n server.tool(\n 'get-vault-risk',\n 'Compute a composite 0-10 risk score for a vault. Score dimensions: TVL magnitude, APY stability, protocol maturity, redeemability, and asset type. Labels: \"low\" (>=7), \"medium\" (>=4), \"high\" (<4). Higher score = safer.',\n {\n slug: z.string().describe('Vault slug in the format \"<chainId>-<vaultAddress>\".'),\n },\n async (params) => {\n try {\n const vault = await forge.vaults.get(params.slug);\n const risk = forge.riskScore(vault);\n return json({ slug: params.slug, name: vault.name, ...risk });\n } catch (err) {\n return error(`Failed to compute risk score: ${(err as Error).message}`);\n }\n },\n );\n\n // ── quote-vault-deposit ───────────────────────────────────────────\n\n server.tool(\n 'quote-vault-deposit',\n 'Build a deposit quote for an Earn vault. Requires a LI.FI API key (set LIFI_API_KEY env var). Returns the quote with transaction data ready to sign. IMPORTANT: Before executing the deposit, use check-allowance with the quote\\'s approvalAddress to verify token approval.',\n {\n slug: z.string().describe('Vault slug in the format \"<chainId>-<vaultAddress>\".'),\n wallet: z.string().describe('Wallet address (0x...) that will execute the deposit.'),\n fromAmount: z.string().describe('Human-readable amount to deposit (e.g. \"100\" for 100 USDC).'),\n fromToken: z.string().optional().describe('Override the from-token address. Defaults to the vault\\'s first underlying token.'),\n fromChain: z.number().optional().describe('Override the source chain ID. Defaults to the vault\\'s chain.'),\n slippage: z.number().optional().describe('Slippage tolerance (e.g. 0.03 for 3%). Defaults to API default.'),\n },\n async (params) => {\n try {\n const vault = await forge.vaults.get(params.slug);\n const result = await forge.buildDepositQuote(vault, {\n fromAmount: params.fromAmount,\n wallet: params.wallet,\n fromToken: params.fromToken,\n fromChain: params.fromChain,\n slippage: params.slippage,\n });\n return json({\n vault: result.vault.name,\n humanAmount: result.humanAmount,\n rawAmount: result.rawAmount,\n decimals: result.decimals,\n estimate: {\n tool: result.quote.estimate.tool,\n toAmount: result.quote.estimate.toAmount,\n toAmountMin: result.quote.estimate.toAmountMin,\n executionDuration: result.quote.estimate.executionDuration,\n gasCosts: result.quote.estimate.gasCosts,\n feeCosts: result.quote.estimate.feeCosts,\n },\n transactionRequest: {\n to: result.quote.transactionRequest.to,\n value: result.quote.transactionRequest.value,\n chainId: result.quote.transactionRequest.chainId,\n },\n });\n } catch (err) {\n return error(`Failed to build deposit quote: ${(err as Error).message}`);\n }\n },\n );\n\n // ── quote-vault-redeem ─────────────────────────────────────────────\n\n server.tool(\n 'quote-vault-redeem',\n 'Build a withdrawal/redeem quote for an Earn vault. Withdraws vault share tokens back to the underlying asset. Requires LIFI_API_KEY. Checks isRedeemable before quoting.',\n {\n slug: z.string().describe('Vault slug \"<chainId>-<vaultAddress>\".'),\n wallet: z.string().describe('Wallet address (0x...).'),\n fromAmount: z.string().describe('Amount of vault share tokens to redeem (human-readable).'),\n toToken: z.string().optional().describe('Override destination token address.'),\n toChain: z.number().optional().describe('Override destination chain ID.'),\n slippage: z.number().optional().describe('Slippage tolerance.'),\n },\n async (params) => {\n try {\n const vault = await forge.vaults.get(params.slug);\n const result = await forge.buildRedeemQuote(vault, {\n fromAmount: params.fromAmount,\n wallet: params.wallet,\n toToken: params.toToken,\n toChain: params.toChain,\n slippage: params.slippage,\n });\n return json({\n vault: result.vault.name,\n humanAmount: result.humanAmount,\n rawAmount: result.rawAmount,\n isRedeemable: vault.isRedeemable,\n estimate: {\n toAmount: result.quote.estimate.toAmount,\n toAmountMin: result.quote.estimate.toAmountMin,\n executionDuration: result.quote.estimate.executionDuration,\n },\n transactionRequest: {\n to: result.quote.transactionRequest.to,\n value: result.quote.transactionRequest.value,\n chainId: result.quote.transactionRequest.chainId,\n },\n });\n } catch (err) {\n return error(`Failed to build redeem quote: ${(err as Error).message}`);\n }\n },\n );\n\n // ── check-allowance ───────────────────────────────────────────────\n\n server.tool(\n 'check-allowance',\n 'Check ERC-20 token allowance and build an approval tx if needed. Use before depositing — the Composer contract needs token approval. Get the spender from quote.estimate.approvalAddress.',\n {\n rpcUrl: z.string().describe('JSON-RPC endpoint for the chain.'),\n tokenAddress: z.string().describe('ERC-20 token contract address.'),\n owner: z.string().describe('Wallet address (token holder).'),\n spender: z.string().describe('Spender address (from quote.estimate.approvalAddress).'),\n requiredAmount: z.string().describe('Required amount in smallest unit (from quote rawAmount).'),\n chainId: z.number().describe('Chain ID for the approval transaction.'),\n },\n async (params) => {\n try {\n const { checkAllowance, buildApprovalTx, MAX_UINT256 } = await import('@earnforge/sdk');\n const required = BigInt(params.requiredAmount);\n const result = await checkAllowance(\n params.rpcUrl, params.tokenAddress, params.owner, params.spender, required,\n );\n\n const response: Record<string, unknown> = {\n allowance: result.allowance.toString(),\n sufficient: result.sufficient,\n requiredAmount: result.requiredAmount.toString(),\n };\n\n if (!result.sufficient) {\n response.approvalTx = buildApprovalTx(params.tokenAddress, params.spender, MAX_UINT256, params.chainId);\n response.note = 'Allowance insufficient. Sign the approvalTx before depositing.';\n }\n\n return json(response);\n } catch (err) {\n return error(`Failed to check allowance: ${(err as Error).message}`);\n }\n },\n );\n\n // ── suggest-allocation ────────────────────────────────────────────\n\n server.tool(\n 'suggest-allocation',\n 'Get a portfolio allocation suggestion. Uses a risk-adjusted scoring engine to recommend how to split funds across multiple vaults. Returns allocation percentages, expected APY, and risk scores per vault.',\n {\n amount: z.number().describe('Total USD amount to allocate across vaults.'),\n asset: z.string().optional().describe('Filter vaults by underlying token symbol (e.g. \"USDC\").'),\n maxChains: z.number().optional().describe('Maximum number of different chains to spread across (default 5).'),\n maxVaults: z.number().optional().describe('Maximum number of vaults in the allocation (default 5).'),\n strategy: z.enum(['conservative', 'max-apy', 'diversified', 'risk-adjusted']).optional()\n .describe('Strategy preset to guide allocation.'),\n },\n async (params) => {\n try {\n const result = await forge.suggest({\n amount: params.amount,\n asset: params.asset,\n maxChains: params.maxChains,\n maxVaults: params.maxVaults,\n strategy: params.strategy as StrategyPreset | undefined,\n });\n return json({\n totalAmount: result.totalAmount,\n expectedApy: result.expectedApy,\n allocations: result.allocations.map((a) => ({\n vault: a.vault.name,\n slug: a.vault.slug,\n chainId: a.vault.chainId,\n protocol: a.vault.protocol.name,\n percentage: a.percentage,\n amount: a.amount,\n apy: a.apy,\n riskScore: a.risk.score,\n riskLabel: a.risk.label,\n })),\n });\n } catch (err) {\n return error(`Failed to generate allocation suggestion: ${(err as Error).message}`);\n }\n },\n );\n\n // ── run-doctor ────────────────────────────────────────────────────\n\n server.tool(\n 'run-doctor',\n 'Run preflight / pitfall checks on a vault before depositing. Checks: isTransactional, chain mismatch, gas token balance, token balance sufficiency, underlyingTokens existence, and redeemability. Returns a pass/fail report with detailed issues.',\n {\n slug: z.string().describe('Vault slug in the format \"<chainId>-<vaultAddress>\".'),\n wallet: z.string().describe('Wallet address (0x...) to run checks against.'),\n walletChainId: z.number().optional().describe('Chain ID the wallet is currently connected to. Used to detect chain mismatch.'),\n depositAmount: z.string().optional().describe('Human-readable amount to deposit. Used for balance sufficiency check.'),\n },\n async (params) => {\n try {\n const vault = await forge.vaults.get(params.slug);\n const report = forge.preflight(vault, params.wallet, {\n walletChainId: params.walletChainId,\n depositAmount: params.depositAmount,\n });\n return json({\n ok: report.ok,\n vault: report.vault.name,\n slug: report.vault.slug,\n wallet: report.wallet,\n issueCount: report.issues.length,\n issues: report.issues,\n });\n } catch (err) {\n return error(`Failed to run doctor: ${(err as Error).message}`);\n }\n },\n );\n\n return server;\n}\n\n// ── Vault summary helper ────────────────────────────────────────────\n\nfunction summarizeVault(vault: Vault) {\n return {\n name: vault.name,\n slug: vault.slug,\n chainId: vault.chainId,\n network: vault.network,\n protocol: vault.protocol.name,\n tags: vault.tags,\n apy: {\n total: vault.analytics.apy.total,\n base: vault.analytics.apy.base,\n reward: vault.analytics.apy.reward,\n },\n tvlUsd: vault.analytics.tvl.usd,\n underlyingTokens: vault.underlyingTokens.map((t) => t.symbol),\n isTransactional: vault.isTransactional,\n isRedeemable: vault.isRedeemable,\n };\n}\n\n// ── Main ────────────────────────────────────────────────────────────\n\nexport async function main(): Promise<void> {\n const server = createServer();\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n\n// Only auto-start when run directly as CLI, not when imported for testing.\n// In vitest, import.meta.url will not match the process entry point.\nconst entrypoint = process.argv[1] ?? '';\nconst isMain =\n import.meta.url === `file://${entrypoint}` ||\n entrypoint.endsWith('earnforge-mcp');\n\nif (isMain) {\n main().catch((err) => {\n console.error('EarnForge MCP server failed to start:', err);\n process.exit(1);\n });\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@earnforge/mcp",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "Apache-2.0",
6
+ "description": "Earn-native MCP server for the LI.FI Earn API — 9 tools for vault discovery, risk scoring, and deposit quoting",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/esm/index.js",
10
+ "types": "./dist/esm/index.d.ts"
11
+ }
12
+ },
13
+ "bin": {
14
+ "earnforge-mcp": "./dist/esm/index.js"
15
+ },
16
+ "files": ["dist", "README.md", "LICENSE"],
17
+ "scripts": {
18
+ "build": "tsdown src/index.ts --format esm --dts --out-dir dist/esm",
19
+ "typecheck": "tsc --noEmit",
20
+ "test": "vitest run",
21
+ "test:unit": "vitest run",
22
+ "clean": "rm -rf dist .turbo"
23
+ },
24
+ "dependencies": {
25
+ "@earnforge/sdk": "workspace:*",
26
+ "@modelcontextprotocol/sdk": "^1.12.1",
27
+ "zod": "^3.25.0 || ^4.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "tsdown": "^0.21.7",
31
+ "typescript": "^5.9.3",
32
+ "vitest": "^4.1.4"
33
+ }
34
+ }