@lendwise/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.
- package/README.md +76 -0
- package/dist/bin/stdio.d.ts +2 -0
- package/dist/bin/stdio.js +18 -0
- package/dist/core/config.d.ts +18 -0
- package/dist/core/config.js +19 -0
- package/dist/core/graphql/client.d.ts +3 -0
- package/dist/core/graphql/client.js +14 -0
- package/dist/core/graphql/queries.d.ts +133 -0
- package/dist/core/graphql/queries.js +207 -0
- package/dist/core/http.d.ts +20 -0
- package/dist/core/http.js +72 -0
- package/dist/core/optimizer.d.ts +58 -0
- package/dist/core/optimizer.js +77 -0
- package/dist/core/server.d.ts +12 -0
- package/dist/core/server.js +115 -0
- package/dist/core/stats.d.ts +24 -0
- package/dist/core/stats.js +31 -0
- package/dist/core/tools/find-best-markets.d.ts +32 -0
- package/dist/core/tools/find-best-markets.js +57 -0
- package/dist/core/tools/get-market-details.d.ts +74 -0
- package/dist/core/tools/get-market-details.js +70 -0
- package/dist/core/tools/get-market-history.d.ts +44 -0
- package/dist/core/tools/get-market-history.js +58 -0
- package/dist/core/tools/list-market-universe.d.ts +31 -0
- package/dist/core/tools/list-market-universe.js +33 -0
- package/dist/core/tools/optimize-allocation.d.ts +42 -0
- package/dist/core/tools/optimize-allocation.js +96 -0
- package/dist/core/tools/shared.d.ts +28 -0
- package/dist/core/tools/shared.js +41 -0
- package/package.json +33 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const optimizeAllocationArgs: {
|
|
3
|
+
amountUsd: z.ZodNumber;
|
|
4
|
+
productIds: z.ZodArray<z.ZodString>;
|
|
5
|
+
diversification: z.ZodDefault<z.ZodNumber>;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Allocate `amountUsd` across the given markets via the solver.
|
|
9
|
+
*
|
|
10
|
+
* The solver speaks positions, not ids: we hand it `apy[]` and it hands back
|
|
11
|
+
* `vault_index`. Both directions run off the single array built by
|
|
12
|
+
* `buildApyVector`, so the index it returns cannot drift from the product it
|
|
13
|
+
* meant. Getting this wrong would attribute real money to the wrong market and
|
|
14
|
+
* nothing downstream would notice — hence the unit tests.
|
|
15
|
+
*/
|
|
16
|
+
export declare function optimizeAllocation(raw: unknown): Promise<{
|
|
17
|
+
error: string;
|
|
18
|
+
missing: string[];
|
|
19
|
+
disclaimer: string;
|
|
20
|
+
} | {
|
|
21
|
+
disclaimer: string;
|
|
22
|
+
excluded?: {
|
|
23
|
+
productIds: string[];
|
|
24
|
+
reason: string;
|
|
25
|
+
} | undefined;
|
|
26
|
+
amountUsd: number;
|
|
27
|
+
diversification: number;
|
|
28
|
+
allocations: {
|
|
29
|
+
productId: string;
|
|
30
|
+
amountUsd: number;
|
|
31
|
+
allocationPercent: number;
|
|
32
|
+
netApy: number;
|
|
33
|
+
}[];
|
|
34
|
+
blendedNetApy: number;
|
|
35
|
+
projected: {
|
|
36
|
+
months: number;
|
|
37
|
+
yieldUsd: number;
|
|
38
|
+
caveat: string;
|
|
39
|
+
};
|
|
40
|
+
error?: undefined;
|
|
41
|
+
missing?: undefined;
|
|
42
|
+
}>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { NOT_ADVICE } from '../config.js';
|
|
3
|
+
import { gql } from '../graphql/client.js';
|
|
4
|
+
import { LATEST_SUPPLY_BY_PRODUCTS, } from '../graphql/queries.js';
|
|
5
|
+
import { blendedApy, buildApyVector, mapAllocations, optimizeVaults, } from '../optimizer.js';
|
|
6
|
+
import { finite } from './shared.js';
|
|
7
|
+
export const optimizeAllocationArgs = {
|
|
8
|
+
amountUsd: z
|
|
9
|
+
.number()
|
|
10
|
+
.positive()
|
|
11
|
+
.describe('Total amount to allocate, in USD.'),
|
|
12
|
+
productIds: z
|
|
13
|
+
.array(z.string().min(1))
|
|
14
|
+
.min(2)
|
|
15
|
+
.max(20)
|
|
16
|
+
.describe('Markets to allocate across, from find_best_markets. Order is not ' +
|
|
17
|
+
'significant to you, but is preserved internally.'),
|
|
18
|
+
diversification: z
|
|
19
|
+
.number()
|
|
20
|
+
.min(0)
|
|
21
|
+
.max(100)
|
|
22
|
+
.default(80)
|
|
23
|
+
.describe('Target diversification score. 80 = highly diversified, 0 = concentrate ' +
|
|
24
|
+
'everything in the highest yield.'),
|
|
25
|
+
};
|
|
26
|
+
const Args = z.object(optimizeAllocationArgs);
|
|
27
|
+
/** Months of projected yield to report — matches the 6-month horizon the tool is for. */
|
|
28
|
+
const PROJECTION_MONTHS = 6;
|
|
29
|
+
/**
|
|
30
|
+
* Allocate `amountUsd` across the given markets via the solver.
|
|
31
|
+
*
|
|
32
|
+
* The solver speaks positions, not ids: we hand it `apy[]` and it hands back
|
|
33
|
+
* `vault_index`. Both directions run off the single array built by
|
|
34
|
+
* `buildApyVector`, so the index it returns cannot drift from the product it
|
|
35
|
+
* meant. Getting this wrong would attribute real money to the wrong market and
|
|
36
|
+
* nothing downstream would notice — hence the unit tests.
|
|
37
|
+
*/
|
|
38
|
+
export async function optimizeAllocation(raw) {
|
|
39
|
+
const { amountUsd, productIds, diversification } = Args.parse(raw);
|
|
40
|
+
// De-duplicate: the same product twice would occupy two positions and let the
|
|
41
|
+
// solver "diversify" across what is really one market.
|
|
42
|
+
const unique = [...new Set(productIds)];
|
|
43
|
+
// ONE request for the whole batch, via the server-side productIds filter. A
|
|
44
|
+
// request per product would spend up to 20 of the caller's 60 req/min budget
|
|
45
|
+
// on a single optimize_allocation, and rate-limit the agent mid-flow.
|
|
46
|
+
const data = await gql(LATEST_SUPPLY_BY_PRODUCTS, { productIds: unique, first: unique.length });
|
|
47
|
+
const latestApyByProduct = new Map();
|
|
48
|
+
for (const row of data.latestSupplyApy.items) {
|
|
49
|
+
const net = finite(row.apy.net);
|
|
50
|
+
if (net !== null)
|
|
51
|
+
latestApyByProduct.set(row.productId, net);
|
|
52
|
+
}
|
|
53
|
+
const { apys, found, missing } = buildApyVector(unique, latestApyByProduct);
|
|
54
|
+
if (found.length < 2) {
|
|
55
|
+
return {
|
|
56
|
+
error: 'Need at least 2 markets with a current APY to allocate across. ' +
|
|
57
|
+
`Only ${found.length} of ${unique.length} had a usable snapshot.`,
|
|
58
|
+
missing,
|
|
59
|
+
disclaimer: NOT_ADVICE,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const response = await optimizeVaults(apys, diversification);
|
|
63
|
+
// `found` is the array the apys were built from, so vault_index indexes it.
|
|
64
|
+
const allocations = mapAllocations(found, apys, response.allocations, amountUsd).filter((a) => a.amountUsd > 0);
|
|
65
|
+
const blended = blendedApy(allocations);
|
|
66
|
+
return {
|
|
67
|
+
amountUsd,
|
|
68
|
+
diversification: response.resulting_diversification,
|
|
69
|
+
allocations: allocations
|
|
70
|
+
.sort((a, b) => b.amountUsd - a.amountUsd)
|
|
71
|
+
.map((a) => ({
|
|
72
|
+
productId: a.productId,
|
|
73
|
+
amountUsd: Number(a.amountUsd.toFixed(2)),
|
|
74
|
+
allocationPercent: a.allocationPercent,
|
|
75
|
+
netApy: a.apy,
|
|
76
|
+
})),
|
|
77
|
+
blendedNetApy: blended,
|
|
78
|
+
projected: {
|
|
79
|
+
months: PROJECTION_MONTHS,
|
|
80
|
+
// Simple pro-rata of the annual rate. The APYs are variable, so compounding
|
|
81
|
+
// precision here would be false precision.
|
|
82
|
+
yieldUsd: Number((amountUsd * blended * (PROJECTION_MONTHS / 12)).toFixed(2)),
|
|
83
|
+
caveat: 'Projection assumes today’s APYs hold for the whole period. They will ' +
|
|
84
|
+
'not — check get_market_history for how much each one actually moves.',
|
|
85
|
+
},
|
|
86
|
+
...(missing.length > 0
|
|
87
|
+
? {
|
|
88
|
+
excluded: {
|
|
89
|
+
productIds: missing,
|
|
90
|
+
reason: 'No current APY snapshot; excluded rather than assumed to be zero.',
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
: {}),
|
|
94
|
+
disclaimer: NOT_ADVICE,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { SnapshotRow } from '../graphql/queries.js';
|
|
2
|
+
/** Coerce a non-finite number to null. Never let NaN out of a tool. */
|
|
3
|
+
export declare function finite(v: number | null | undefined): number | null;
|
|
4
|
+
export interface DescribedMarket {
|
|
5
|
+
productId: string;
|
|
6
|
+
asset: string;
|
|
7
|
+
chain: string;
|
|
8
|
+
chainId: number;
|
|
9
|
+
protocol: string;
|
|
10
|
+
market: string;
|
|
11
|
+
apy: {
|
|
12
|
+
net: number | null;
|
|
13
|
+
base: number | null;
|
|
14
|
+
rewards: number | null;
|
|
15
|
+
fees: number | null;
|
|
16
|
+
};
|
|
17
|
+
tvlUsd: number | null;
|
|
18
|
+
utilizationRate: number | null;
|
|
19
|
+
/** Snapshot time — every response says how fresh it is. */
|
|
20
|
+
asOf: string;
|
|
21
|
+
quality: {
|
|
22
|
+
status: string;
|
|
23
|
+
slots: string;
|
|
24
|
+
reliable: boolean;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export declare function describeRow(row: SnapshotRow): DescribedMarket;
|
|
28
|
+
export declare function isUnreliable(m: DescribedMarket): boolean;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Coerce a non-finite number to null. Never let NaN out of a tool. */
|
|
2
|
+
export function finite(v) {
|
|
3
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : null;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* A row's data quality. `quality.count` is how many 10-minute slots of the
|
|
7
|
+
* current hour have landed; below half the expected count the hour's average is
|
|
8
|
+
* built on too little and is flagged rather than silently returned as fact.
|
|
9
|
+
*/
|
|
10
|
+
function reliability(row) {
|
|
11
|
+
const { count, expectedCount, status } = row.quality;
|
|
12
|
+
const completeness = expectedCount > 0 ? count / expectedCount : 0;
|
|
13
|
+
return {
|
|
14
|
+
status,
|
|
15
|
+
slots: `${count}/${expectedCount}`,
|
|
16
|
+
reliable: completeness >= 0.5,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function describeRow(row) {
|
|
20
|
+
return {
|
|
21
|
+
productId: row.productId,
|
|
22
|
+
asset: row.asset,
|
|
23
|
+
chain: row.product?.protocol.chain.name ?? String(row.chainId),
|
|
24
|
+
chainId: row.chainId,
|
|
25
|
+
protocol: row.product?.protocol.provider ?? 'unknown',
|
|
26
|
+
market: row.product?.protocol.name ?? 'unknown',
|
|
27
|
+
apy: {
|
|
28
|
+
net: finite(row.apy.net),
|
|
29
|
+
base: finite(row.apy.base),
|
|
30
|
+
rewards: finite(row.apy.rewards),
|
|
31
|
+
fees: finite(row.apy.fees),
|
|
32
|
+
},
|
|
33
|
+
tvlUsd: finite(row.market.supplyAssetsUsd),
|
|
34
|
+
utilizationRate: finite(row.market.utilizationRate),
|
|
35
|
+
asOf: row.hour,
|
|
36
|
+
quality: reliability(row),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function isUnreliable(m) {
|
|
40
|
+
return !m.quality.reliable;
|
|
41
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lendwise/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Lendwise — compare DeFi supply/borrow markets across Aave, Morpho and Compound on 27 standardized chains.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=24"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"lendwise-mcp": "./dist/bin/stdio.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.22.0",
|
|
18
|
+
"mcp-handler": "^1.0.2",
|
|
19
|
+
"zod": "^4.3.6"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^24.9.2",
|
|
23
|
+
"typescript": "^5.9.3",
|
|
24
|
+
"vitest": "^4.1.7"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc",
|
|
28
|
+
"typecheck": "tsc --noEmit -p tsconfig.check.json",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"test:watch": "vitest",
|
|
31
|
+
"start": "node dist/bin/stdio.js"
|
|
32
|
+
}
|
|
33
|
+
}
|