@defipipe/mcp 0.1.0 → 0.1.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/http.d.ts +1 -0
- package/dist/http.js +49 -0
- package/dist/index.js +3 -131
- package/dist/server.d.ts +6 -0
- package/dist/server.js +157 -0
- package/package.json +1 -1
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Remote MCP entry (mcp.defipipe.io): stateless Streamable HTTP behind Caddy.
|
|
2
|
+
* A fresh server+transport per request (SDK stateless guidance) so concurrent
|
|
3
|
+
* clients cannot cross request ids; the catalog cache is module-level in
|
|
4
|
+
* server.ts and survives across requests. */
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
7
|
+
import { buildServer } from "./server.js";
|
|
8
|
+
const PORT = Number(process.env.MCP_PORT ?? 3002);
|
|
9
|
+
const httpServer = createServer(async (req, res) => {
|
|
10
|
+
const path = new URL(req.url ?? "/", "http://localhost").pathname;
|
|
11
|
+
if (path === "/health") {
|
|
12
|
+
res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ ok: true }));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (path !== "/" && path !== "/mcp") {
|
|
16
|
+
res.writeHead(404).end();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (req.method !== "POST") {
|
|
20
|
+
res
|
|
21
|
+
.writeHead(405, { "content-type": "application/json", allow: "POST" })
|
|
22
|
+
.end(JSON.stringify({ error: "stateless MCP endpoint: POST JSON-RPC here", docs: "https://defipipe.io/docs#mcp" }));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const chunks = [];
|
|
27
|
+
for await (const c of req)
|
|
28
|
+
chunks.push(c);
|
|
29
|
+
const body = JSON.parse(Buffer.concat(chunks).toString() || "{}");
|
|
30
|
+
const server = buildServer();
|
|
31
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
32
|
+
res.on("close", () => {
|
|
33
|
+
void transport.close();
|
|
34
|
+
void server.close();
|
|
35
|
+
});
|
|
36
|
+
await server.connect(transport);
|
|
37
|
+
await transport.handleRequest(req, res, body);
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
if (!res.headersSent) {
|
|
41
|
+
res
|
|
42
|
+
.writeHead(500, { "content-type": "application/json" })
|
|
43
|
+
.end(JSON.stringify({ error: e.message }));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
httpServer.listen(PORT, "127.0.0.1", () => {
|
|
48
|
+
console.error(`defipipe mcp: streamable http on 127.0.0.1:${PORT}`);
|
|
49
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -1,133 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
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";
|
|
2
|
+
/** stdio entry (npx @defipipe/mcp): local MCP for Claude Code/Desktop/Cowork. */
|
|
7
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
-
import {
|
|
9
|
-
|
|
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());
|
|
4
|
+
import { buildServer } from "./server.js";
|
|
5
|
+
await buildServer().connect(new StdioServerTransport());
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** MCP server for defipipe (stdio). Thin wrapper over the public HTTPS API:
|
|
2
|
+
* search the dataset catalog, query aligned time series, raw per-block rows,
|
|
3
|
+
* and tier entitlements. Set DEFIPIPE_API_KEY for keyed tiers; the free tier
|
|
4
|
+
* works without one. */
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
export declare function buildServer(): McpServer;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/** MCP server for defipipe (stdio). Thin wrapper over the public HTTPS API:
|
|
2
|
+
* search the dataset catalog, query aligned time series, raw per-block rows,
|
|
3
|
+
* and tier entitlements. Set DEFIPIPE_API_KEY for keyed tiers; the free tier
|
|
4
|
+
* works without one. */
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
const API = process.env.DEFIPIPE_API_URL ?? "https://api.defipipe.io";
|
|
8
|
+
const SITE = process.env.DEFIPIPE_SITE_URL ?? "https://defipipe.io";
|
|
9
|
+
const KEY = process.env.DEFIPIPE_API_KEY;
|
|
10
|
+
const VERSION = "0.1.1";
|
|
11
|
+
function headers() {
|
|
12
|
+
return KEY ? { authorization: `Bearer ${KEY}` } : {};
|
|
13
|
+
}
|
|
14
|
+
/** Fetch a defipipe endpoint. Non-2xx returns the API's JSON error body as a
|
|
15
|
+
* tool error (tier/upgrade/retry hints intact); network-level failures return
|
|
16
|
+
* an equally actionable error instead of a bare "fetch failed". Agents recover
|
|
17
|
+
* from actionable errors; they flail on bare ones. */
|
|
18
|
+
async function apiGet(url) {
|
|
19
|
+
try {
|
|
20
|
+
const res = await fetch(url, { headers: headers() });
|
|
21
|
+
const text = await res.text();
|
|
22
|
+
return { ok: res.ok, text };
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
return { ok: false, text: networkError(url, e) };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function networkError(url, e) {
|
|
29
|
+
const cause = e.cause?.code ?? e.message;
|
|
30
|
+
return JSON.stringify({
|
|
31
|
+
error: "network_unreachable",
|
|
32
|
+
endpoint: url,
|
|
33
|
+
cause,
|
|
34
|
+
hint: "The defipipe API could not be reached from this environment. If you are in a " +
|
|
35
|
+
"sandbox, its network allowlist likely blocks api.defipipe.io/defipipe.io; " +
|
|
36
|
+
"otherwise retry, and check https://defipipe.io/docs if it persists.",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function toolResult(r) {
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: "text", text: r.text }],
|
|
42
|
+
isError: !r.ok,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
let catalogCache = null;
|
|
46
|
+
async function loadCatalog() {
|
|
47
|
+
if (catalogCache && Date.now() - catalogCache.at < 5 * 60_000)
|
|
48
|
+
return catalogCache.datasets;
|
|
49
|
+
const res = await fetch(`${SITE}/api/search-index`);
|
|
50
|
+
if (!res.ok)
|
|
51
|
+
throw new Error(`catalog fetch failed: ${res.status}`);
|
|
52
|
+
const body = (await res.json());
|
|
53
|
+
catalogCache = { at: Date.now(), datasets: body.datasets };
|
|
54
|
+
return body.datasets;
|
|
55
|
+
}
|
|
56
|
+
export function buildServer() {
|
|
57
|
+
const server = new McpServer({ name: "defipipe", version: VERSION });
|
|
58
|
+
server.registerTool("search_datasets", {
|
|
59
|
+
title: "Search defipipe datasets",
|
|
60
|
+
description: "Search the defipipe catalog of historical per-block DeFi datasets. Returns matching " +
|
|
61
|
+
"datasets with their canonical series IDs, which are the inputs to get_aligned and " +
|
|
62
|
+
"get_series_raw. Omit the query to list every dataset.",
|
|
63
|
+
inputSchema: {
|
|
64
|
+
query: z
|
|
65
|
+
.string()
|
|
66
|
+
.optional()
|
|
67
|
+
.describe("Case-insensitive match against protocol, label, category, and metrics (e.g. 'aave', 'borrow rate', 'lending')"),
|
|
68
|
+
},
|
|
69
|
+
}, async ({ query }) => {
|
|
70
|
+
let datasets;
|
|
71
|
+
try {
|
|
72
|
+
datasets = await loadCatalog();
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
return { content: [{ type: "text", text: networkError(`${SITE}/api/search-index`, e) }], isError: true };
|
|
76
|
+
}
|
|
77
|
+
const q = query?.toLowerCase().trim();
|
|
78
|
+
const hits = !q
|
|
79
|
+
? datasets
|
|
80
|
+
: datasets.filter((d) => [d.ns, d.label, d.category, ...d.metrics, ...d.series.map((s) => s.metric)]
|
|
81
|
+
.join(" ")
|
|
82
|
+
.toLowerCase()
|
|
83
|
+
.includes(q));
|
|
84
|
+
const out = hits.map((d) => ({
|
|
85
|
+
dataset: `${d.ns} · ${d.label}`,
|
|
86
|
+
category: d.category,
|
|
87
|
+
page: `${SITE}${d.href}`,
|
|
88
|
+
series: d.series,
|
|
89
|
+
}));
|
|
90
|
+
return {
|
|
91
|
+
content: [{ type: "text", text: JSON.stringify({ count: out.length, datasets: out }, null, 2) }],
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
server.registerTool("get_aligned", {
|
|
95
|
+
title: "Query aligned time series",
|
|
96
|
+
description: "Resample up to 10 series onto one shared UTC time axis (GET /v1/aligned). Each value " +
|
|
97
|
+
"is the last observation at or before the interval boundary: no interpolation, no " +
|
|
98
|
+
"lookahead, safe for backtests. Free tier: 5m resolution and coarser, trailing 90 days.",
|
|
99
|
+
inputSchema: {
|
|
100
|
+
series: z.array(z.string()).min(1).max(10).describe("Canonical series IDs (from search_datasets)"),
|
|
101
|
+
freq: z.enum(["1m", "5m", "15m", "1h", "4h", "1d", "7d"]).optional().describe("Bar size, default 1h. 1m requires an API key."),
|
|
102
|
+
from: z.string().optional().describe("ISO 8601 start, default: to minus 7 days"),
|
|
103
|
+
to: z.string().optional().describe("ISO 8601 end, default: now"),
|
|
104
|
+
},
|
|
105
|
+
}, async ({ series, freq, from, to }) => {
|
|
106
|
+
const p = new URLSearchParams({ series: series.join(",") });
|
|
107
|
+
if (freq)
|
|
108
|
+
p.set("freq", freq);
|
|
109
|
+
if (from)
|
|
110
|
+
p.set("from", from);
|
|
111
|
+
if (to)
|
|
112
|
+
p.set("to", to);
|
|
113
|
+
return toolResult(await apiGet(`${API}/v1/aligned?${p}`));
|
|
114
|
+
});
|
|
115
|
+
server.registerTool("get_series_raw", {
|
|
116
|
+
title: "Query raw per-block rows",
|
|
117
|
+
description: "Raw per-block rows for one series (GET /v1/series/:id). Each row is " +
|
|
118
|
+
"[block_number, block_timestamp, value_raw, value_decimal]; value_raw is the exact " +
|
|
119
|
+
"uint256-safe on-chain value as a string. Requires an API key (DEFIPIPE_API_KEY). " +
|
|
120
|
+
"Paginate by passing the response's next_after_block as after_block.",
|
|
121
|
+
inputSchema: {
|
|
122
|
+
id: z.string().describe("Canonical series ID"),
|
|
123
|
+
from: z.string().optional().describe("ISO 8601 start, default: to minus 24 hours"),
|
|
124
|
+
to: z.string().optional().describe("ISO 8601 end, default: now"),
|
|
125
|
+
after_block: z.number().int().optional().describe("Pagination cursor: only rows with a strictly greater block number"),
|
|
126
|
+
limit: z.number().int().min(1).max(10000).optional().describe("Rows per page, default 1,000"),
|
|
127
|
+
},
|
|
128
|
+
}, async ({ id, from, to, after_block, limit }) => {
|
|
129
|
+
const p = new URLSearchParams();
|
|
130
|
+
if (from)
|
|
131
|
+
p.set("from", from);
|
|
132
|
+
if (to)
|
|
133
|
+
p.set("to", to);
|
|
134
|
+
if (after_block !== undefined)
|
|
135
|
+
p.set("after_block", String(after_block));
|
|
136
|
+
if (limit !== undefined)
|
|
137
|
+
p.set("limit", String(limit));
|
|
138
|
+
const qs = p.size ? `?${p}` : "";
|
|
139
|
+
return toolResult(await apiGet(`${API}/v1/series/${encodeURIComponent(id)}${qs}`));
|
|
140
|
+
});
|
|
141
|
+
server.registerTool("get_limits", {
|
|
142
|
+
title: "Check tier entitlements",
|
|
143
|
+
description: "Machine-readable entitlements for the current credential (GET /v1/limits): rate " +
|
|
144
|
+
"limits, allowed frequencies, raw-row access, and history window. Call this before " +
|
|
145
|
+
"large pulls instead of discovering limits through 429s.",
|
|
146
|
+
inputSchema: {},
|
|
147
|
+
}, async () => toolResult(await apiGet(`${API}/v1/limits`)));
|
|
148
|
+
server.registerResource("docs", "defipipe://docs", {
|
|
149
|
+
title: "defipipe API reference",
|
|
150
|
+
description: "Full API reference as markdown: series-ID grammar, endpoints, tiers, errors, Python SDK.",
|
|
151
|
+
mimeType: "text/markdown",
|
|
152
|
+
}, async (uri) => {
|
|
153
|
+
const res = await fetch(`${SITE}/docs.md`);
|
|
154
|
+
return { contents: [{ uri: uri.href, text: await res.text(), mimeType: "text/markdown" }] };
|
|
155
|
+
});
|
|
156
|
+
return server;
|
|
157
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defipipe/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
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
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|