@defipipe/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 +74 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +133 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# @defipipe/mcp
|
|
2
|
+
|
|
3
|
+
MCP server for [defipipe](https://defipipe.io): historical, point-in-time DeFi contract
|
|
4
|
+
state. What read functions returned at every Ethereum block (Aave rates, Uniswap prices,
|
|
5
|
+
Lido supply), served as canonical per-block time series.
|
|
6
|
+
|
|
7
|
+
Gives any MCP client (Claude Code, Claude Desktop, Cursor, ...) tools to search the dataset
|
|
8
|
+
catalog and query aligned time series without leaving the conversation. The free tier works
|
|
9
|
+
with no API key.
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
claude mcp add defipipe -- npx -y @defipipe/mcp
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or in any MCP client config:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"mcpServers": {
|
|
22
|
+
"defipipe": {
|
|
23
|
+
"command": "npx",
|
|
24
|
+
"args": ["-y", "@defipipe/mcp"],
|
|
25
|
+
"env": { "DEFIPIPE_API_KEY": "dp_..." }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`DEFIPIPE_API_KEY` is optional: without it you run on the free tier (aligned queries at 5m
|
|
32
|
+
resolution and coarser, trailing 90 days, no raw rows). Keys come from the
|
|
33
|
+
[dashboard](https://defipipe.io/dashboard); tiers are on the
|
|
34
|
+
[pricing page](https://defipipe.io/pricing).
|
|
35
|
+
|
|
36
|
+
## Tools
|
|
37
|
+
|
|
38
|
+
| Tool | What it does |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `search_datasets` | Search the catalog (protocol, metric, category). Returns datasets with their canonical series IDs. |
|
|
41
|
+
| `get_aligned` | Up to 10 series resampled onto one shared UTC time axis. Last value at or before each boundary: no interpolation, no lookahead, safe for backtests. |
|
|
42
|
+
| `get_series_raw` | Raw per-block rows for one series: `[block_number, block_timestamp, value_raw, value_decimal]` with the exact uint256 value as a string. Requires an API key. |
|
|
43
|
+
| `get_limits` | Machine-readable entitlements for the current credential: rate limits, allowed frequencies, history window. |
|
|
44
|
+
|
|
45
|
+
Plus a `defipipe://docs` resource with the full API reference as markdown.
|
|
46
|
+
|
|
47
|
+
## Series IDs
|
|
48
|
+
|
|
49
|
+
Every series is named by one canonical ID:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
{chain}:{protocol}:{version}:{instance}[/{scope}]:{kind}:{metric}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
For example, the Aave V3 WETH variable borrow rate:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
eth:aave:v3:0x87870bca3f3fd6335c3f4ce8392d69350b4fa4e2/weth:call:getreservedata[currentvariableborrowrate]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Use `search_datasets` to find IDs, or browse the [catalog](https://defipipe.io/datasets).
|
|
62
|
+
|
|
63
|
+
## Environment
|
|
64
|
+
|
|
65
|
+
| Variable | Default | Purpose |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `DEFIPIPE_API_KEY` | none (free tier) | Bearer token for Analyst/Quant tiers |
|
|
68
|
+
| `DEFIPIPE_API_URL` | `https://api.defipipe.io` | API base override |
|
|
69
|
+
| `DEFIPIPE_SITE_URL` | `https://defipipe.io` | Catalog/docs base override |
|
|
70
|
+
|
|
71
|
+
## Related
|
|
72
|
+
|
|
73
|
+
- [API docs](https://defipipe.io/docs) ([markdown](https://defipipe.io/docs.md))
|
|
74
|
+
- Python SDK: `pip install defipipe`
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** MCP server for defipipe (stdio). Thin wrapper over the public HTTPS API:
|
|
3
|
+
* search the dataset catalog, query aligned time series, raw per-block rows,
|
|
4
|
+
* and tier entitlements. Set DEFIPIPE_API_KEY for keyed tiers; the free tier
|
|
5
|
+
* works without one. */
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
const API = process.env.DEFIPIPE_API_URL ?? "https://api.defipipe.io";
|
|
10
|
+
const SITE = process.env.DEFIPIPE_SITE_URL ?? "https://defipipe.io";
|
|
11
|
+
const KEY = process.env.DEFIPIPE_API_KEY;
|
|
12
|
+
const VERSION = "0.1.0";
|
|
13
|
+
function headers() {
|
|
14
|
+
return KEY ? { authorization: `Bearer ${KEY}` } : {};
|
|
15
|
+
}
|
|
16
|
+
/** Fetch a defipipe endpoint; on non-2xx return the API's JSON error body as a
|
|
17
|
+
* tool error so agents see tier/upgrade/retry hints instead of a bare throw. */
|
|
18
|
+
async function apiGet(url) {
|
|
19
|
+
const res = await fetch(url, { headers: headers() });
|
|
20
|
+
const text = await res.text();
|
|
21
|
+
return { ok: res.ok, text };
|
|
22
|
+
}
|
|
23
|
+
function toolResult(r) {
|
|
24
|
+
return {
|
|
25
|
+
content: [{ type: "text", text: r.text }],
|
|
26
|
+
isError: !r.ok,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
let catalogCache = null;
|
|
30
|
+
async function loadCatalog() {
|
|
31
|
+
if (catalogCache && Date.now() - catalogCache.at < 5 * 60_000)
|
|
32
|
+
return catalogCache.datasets;
|
|
33
|
+
const res = await fetch(`${SITE}/api/search-index`);
|
|
34
|
+
if (!res.ok)
|
|
35
|
+
throw new Error(`catalog fetch failed: ${res.status}`);
|
|
36
|
+
const body = (await res.json());
|
|
37
|
+
catalogCache = { at: Date.now(), datasets: body.datasets };
|
|
38
|
+
return body.datasets;
|
|
39
|
+
}
|
|
40
|
+
const server = new McpServer({ name: "defipipe", version: VERSION });
|
|
41
|
+
server.registerTool("search_datasets", {
|
|
42
|
+
title: "Search defipipe datasets",
|
|
43
|
+
description: "Search the defipipe catalog of historical per-block DeFi datasets. Returns matching " +
|
|
44
|
+
"datasets with their canonical series IDs, which are the inputs to get_aligned and " +
|
|
45
|
+
"get_series_raw. Omit the query to list every dataset.",
|
|
46
|
+
inputSchema: {
|
|
47
|
+
query: z
|
|
48
|
+
.string()
|
|
49
|
+
.optional()
|
|
50
|
+
.describe("Case-insensitive match against protocol, label, category, and metrics (e.g. 'aave', 'borrow rate', 'lending')"),
|
|
51
|
+
},
|
|
52
|
+
}, async ({ query }) => {
|
|
53
|
+
const datasets = await loadCatalog();
|
|
54
|
+
const q = query?.toLowerCase().trim();
|
|
55
|
+
const hits = !q
|
|
56
|
+
? datasets
|
|
57
|
+
: datasets.filter((d) => [d.ns, d.label, d.category, ...d.metrics, ...d.series.map((s) => s.metric)]
|
|
58
|
+
.join(" ")
|
|
59
|
+
.toLowerCase()
|
|
60
|
+
.includes(q));
|
|
61
|
+
const out = hits.map((d) => ({
|
|
62
|
+
dataset: `${d.ns} · ${d.label}`,
|
|
63
|
+
category: d.category,
|
|
64
|
+
page: `${SITE}${d.href}`,
|
|
65
|
+
series: d.series,
|
|
66
|
+
}));
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: "text", text: JSON.stringify({ count: out.length, datasets: out }, null, 2) }],
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
server.registerTool("get_aligned", {
|
|
72
|
+
title: "Query aligned time series",
|
|
73
|
+
description: "Resample up to 10 series onto one shared UTC time axis (GET /v1/aligned). Each value " +
|
|
74
|
+
"is the last observation at or before the interval boundary: no interpolation, no " +
|
|
75
|
+
"lookahead, safe for backtests. Free tier: 5m resolution and coarser, trailing 90 days.",
|
|
76
|
+
inputSchema: {
|
|
77
|
+
series: z.array(z.string()).min(1).max(10).describe("Canonical series IDs (from search_datasets)"),
|
|
78
|
+
freq: z.enum(["1m", "5m", "15m", "1h", "4h", "1d", "7d"]).optional().describe("Bar size, default 1h. 1m requires an API key."),
|
|
79
|
+
from: z.string().optional().describe("ISO 8601 start, default: to minus 7 days"),
|
|
80
|
+
to: z.string().optional().describe("ISO 8601 end, default: now"),
|
|
81
|
+
},
|
|
82
|
+
}, async ({ series, freq, from, to }) => {
|
|
83
|
+
const p = new URLSearchParams({ series: series.join(",") });
|
|
84
|
+
if (freq)
|
|
85
|
+
p.set("freq", freq);
|
|
86
|
+
if (from)
|
|
87
|
+
p.set("from", from);
|
|
88
|
+
if (to)
|
|
89
|
+
p.set("to", to);
|
|
90
|
+
return toolResult(await apiGet(`${API}/v1/aligned?${p}`));
|
|
91
|
+
});
|
|
92
|
+
server.registerTool("get_series_raw", {
|
|
93
|
+
title: "Query raw per-block rows",
|
|
94
|
+
description: "Raw per-block rows for one series (GET /v1/series/:id). Each row is " +
|
|
95
|
+
"[block_number, block_timestamp, value_raw, value_decimal]; value_raw is the exact " +
|
|
96
|
+
"uint256-safe on-chain value as a string. Requires an API key (DEFIPIPE_API_KEY). " +
|
|
97
|
+
"Paginate by passing the response's next_after_block as after_block.",
|
|
98
|
+
inputSchema: {
|
|
99
|
+
id: z.string().describe("Canonical series ID"),
|
|
100
|
+
from: z.string().optional().describe("ISO 8601 start, default: to minus 24 hours"),
|
|
101
|
+
to: z.string().optional().describe("ISO 8601 end, default: now"),
|
|
102
|
+
after_block: z.number().int().optional().describe("Pagination cursor: only rows with a strictly greater block number"),
|
|
103
|
+
limit: z.number().int().min(1).max(10000).optional().describe("Rows per page, default 1,000"),
|
|
104
|
+
},
|
|
105
|
+
}, async ({ id, from, to, after_block, limit }) => {
|
|
106
|
+
const p = new URLSearchParams();
|
|
107
|
+
if (from)
|
|
108
|
+
p.set("from", from);
|
|
109
|
+
if (to)
|
|
110
|
+
p.set("to", to);
|
|
111
|
+
if (after_block !== undefined)
|
|
112
|
+
p.set("after_block", String(after_block));
|
|
113
|
+
if (limit !== undefined)
|
|
114
|
+
p.set("limit", String(limit));
|
|
115
|
+
const qs = p.size ? `?${p}` : "";
|
|
116
|
+
return toolResult(await apiGet(`${API}/v1/series/${encodeURIComponent(id)}${qs}`));
|
|
117
|
+
});
|
|
118
|
+
server.registerTool("get_limits", {
|
|
119
|
+
title: "Check tier entitlements",
|
|
120
|
+
description: "Machine-readable entitlements for the current credential (GET /v1/limits): rate " +
|
|
121
|
+
"limits, allowed frequencies, raw-row access, and history window. Call this before " +
|
|
122
|
+
"large pulls instead of discovering limits through 429s.",
|
|
123
|
+
inputSchema: {},
|
|
124
|
+
}, async () => toolResult(await apiGet(`${API}/v1/limits`)));
|
|
125
|
+
server.registerResource("docs", "defipipe://docs", {
|
|
126
|
+
title: "defipipe API reference",
|
|
127
|
+
description: "Full API reference as markdown: series-ID grammar, endpoints, tiers, errors, Python SDK.",
|
|
128
|
+
mimeType: "text/markdown",
|
|
129
|
+
}, async (uri) => {
|
|
130
|
+
const res = await fetch(`${SITE}/docs.md`);
|
|
131
|
+
return { contents: [{ uri: uri.href, text: await res.text(), mimeType: "text/markdown" }] };
|
|
132
|
+
});
|
|
133
|
+
await server.connect(new StdioServerTransport());
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@defipipe/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for defipipe: historical, point-in-time DeFi contract state per block. Search datasets, query aligned time series and raw per-block rows.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"defipipe-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"prepublishOnly": "tsc"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"defi",
|
|
22
|
+
"historical-data",
|
|
23
|
+
"time-series",
|
|
24
|
+
"ethereum"
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
28
|
+
"zod": "^3.25.76"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^26.1.0",
|
|
32
|
+
"typescript": "^5.9.3"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://defipipe.io/docs",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/yosefcevallos/defipipe.git",
|
|
38
|
+
"directory": "packages/mcp"
|
|
39
|
+
}
|
|
40
|
+
}
|