wickedapi-mcp 0.4.0 → 0.6.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/dist/rateLimit.js DELETED
@@ -1,22 +0,0 @@
1
- const WINDOW_MS = 60_000;
2
- const PRUNE_ABOVE_ENTRIES = 1000;
3
- // In-memory fixed-window limiter; fine for the single instance this runs as.
4
- // Move the counters to shared storage before running more than one instance.
5
- const hits = new Map();
6
- let lastPrune = 0;
7
- export function rateLimited(key, limit = Number(process.env.RATE_LIMIT_PER_MINUTE || 30), now = Date.now()) {
8
- if (hits.size >= PRUNE_ABOVE_ENTRIES && now - lastPrune > 30_000) {
9
- lastPrune = now;
10
- for (const [k, h] of hits)
11
- if (!h.length || now - h[h.length - 1] > WINDOW_MS)
12
- hits.delete(k);
13
- }
14
- const bucket = (hits.get(key) ?? []).filter((t) => now - t <= WINDOW_MS);
15
- if (bucket.length >= limit) {
16
- hits.set(key, bucket);
17
- return true;
18
- }
19
- bucket.push(now);
20
- hits.set(key, bucket);
21
- return false;
22
- }
package/dist/server.js DELETED
@@ -1,337 +0,0 @@
1
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { z } from "zod";
3
- import { registerSuiteTools } from "./suite.js";
4
- import { registerSuitePrompts } from "./prompts.js";
5
- import { instrument } from "./usage.js";
6
- function envUrl(name, fallback) {
7
- return (process.env[name] || fallback).replace(/\/$/, "");
8
- }
9
- const SERVICES = [
10
- { id: "trading", name: "Trading Data API", baseUrl: envUrl("WICKEDAPI_BASE_URL", "https://api.wickedapi.com") },
11
- { id: "protocol_health", name: "Protocol Health Oracle", baseUrl: envUrl("WICKEDAPI_PROTOCOL_HEALTH_URL", "https://protocol-health-oracle.wickedapi.com") },
12
- { id: "token_risk", name: "Token Risk Oracle", baseUrl: envUrl("WICKEDAPI_TOKEN_RISK_URL", "https://token-risk-oracle.wickedapi.com") },
13
- { id: "broker_sync", name: "Broker Sync API", baseUrl: envUrl("WICKEDAPI_BROKER_SYNC_URL", "https://broker-sync-api.wickedapi.com") },
14
- ];
15
- const SHOWCASE_URL = envUrl("WICKEDAPI_SHOWCASE_URL", "https://wickedapi.com");
16
- const API_KEY = process.env.WICKEDAPI_API_KEY;
17
- const PAYER_KEY = process.env.WICKEDAPI_PAYER_PRIVATE_KEY;
18
- // A few auto-generated tool names collide or read badly (e.g. GET and POST
19
- // both hitting /v1/protocols) — override just those, keyed by
20
- // "METHOD /openapi/path/{param}".
21
- const TOOL_NAME_OVERRIDES = {
22
- protocol_health: {
23
- "GET /v1/protocols": "protocol_health_list_protocols",
24
- "POST /v1/protocols": "protocol_health_track_protocol",
25
- "GET /v1/health/{slug}": "protocol_health_score",
26
- "POST /v1/refresh": "protocol_health_refresh_all",
27
- "POST /v1/ingest": "protocol_health_sync_universe",
28
- },
29
- broker_sync: {
30
- "POST /v1/users": "broker_sync_register_user",
31
- },
32
- };
33
- // If a funded Base-mainnet wallet key is provided, every call auto-pays the
34
- // x402 402 challenge in USDC — no API key or signup needed at all. Loaded
35
- // lazily so a plain API-key setup never has to pull in viem/x402-fetch.
36
- async function buildFetcher() {
37
- if (!PAYER_KEY)
38
- return fetch;
39
- let deps;
40
- try {
41
- deps = await Promise.all([import("viem"), import("viem/accounts"), import("viem/chains"), import("x402-fetch")]);
42
- }
43
- catch {
44
- console.error("[wickedapi-mcp] WICKEDAPI_PAYER_PRIVATE_KEY is set, but the wallet-payment dependencies " +
45
- "(viem, x402-fetch) aren't installed in this distribution. Falling back to plain fetch — " +
46
- "calls without an API key will surface the 402 challenge instead of auto-paying. " +
47
- "Use `npx wickedapi-mcp` for full auto-pay support.");
48
- return fetch;
49
- }
50
- const [{ createWalletClient, http }, { privateKeyToAccount }, { base }, { wrapFetchWithPayment }] = deps;
51
- const account = privateKeyToAccount(PAYER_KEY);
52
- const wallet = createWalletClient({ account, chain: base, transport: http() });
53
- console.error(`[wickedapi-mcp] x402 auto-pay enabled — wallet ${account.address}`);
54
- // x402-fetch's Signer type expects a client extended with viem's public
55
- // actions; the proven pattern in scripts/x402-bot-test.mjs passes a plain
56
- // wallet client the same way — cast rather than pull in publicActions.
57
- return wrapFetchWithPayment(fetch, wallet);
58
- }
59
- function pathSlug(openapiPath) {
60
- return openapiPath
61
- .replace(/^\/v1\//, "")
62
- .replace(/[{}]/g, "")
63
- .replace(/[\/\-]+/g, "_");
64
- }
65
- function toolNameFor(service, method, openapiPath) {
66
- const override = TOOL_NAME_OVERRIDES[service.id]?.[`${method.toUpperCase()} ${openapiPath}`];
67
- if (override)
68
- return override;
69
- const slug = pathSlug(openapiPath);
70
- if (service.id === "trading")
71
- return slug; // legacy naming, already published
72
- return slug === service.id ? service.id : `${service.id}_${slug}`;
73
- }
74
- function zodFor(p) {
75
- const base = p.type === "number" || p.type === "integer"
76
- ? z.coerce.number()
77
- : p.type === "boolean"
78
- ? z.coerce.boolean()
79
- : z.string();
80
- const described = p.description ? base.describe(p.description) : base;
81
- return p.required ? described : described.optional();
82
- }
83
- async function loadServiceSpec(service) {
84
- try {
85
- const res = await fetch(`${service.baseUrl}/openapi.json`);
86
- if (!res.ok)
87
- throw new Error(`HTTP ${res.status}`);
88
- return (await res.json());
89
- }
90
- catch (err) {
91
- console.error(`[wickedapi-mcp] WARNING: couldn't load ${service.name} spec (${service.baseUrl}/openapi.json): ` +
92
- `${err.message} — its tools are unavailable this session, the rest load normally.`);
93
- return null;
94
- }
95
- }
96
- function registerServiceTools(server, service, doc, doFetch) {
97
- let registered = 0;
98
- for (const [openapiPath, methods] of Object.entries(doc.paths)) {
99
- if (!openapiPath.startsWith("/v1/") || openapiPath === "/v1/status")
100
- continue; // /v1/status is a resource instead
101
- if (openapiPath.includes("webhook"))
102
- continue; // called by a third party, never by an agent
103
- for (const [method, op] of Object.entries(methods)) {
104
- const pathParamNames = [];
105
- const queryParamNames = [];
106
- const bodyPropNames = [];
107
- const shape = {};
108
- for (const p of op.parameters ?? []) {
109
- if (p.in === "path") {
110
- pathParamNames.push(p.name);
111
- shape[p.name] = zodFor({ type: p.schema?.type, required: true, description: p.description });
112
- }
113
- else if (p.in === "query") {
114
- queryParamNames.push(p.name);
115
- shape[p.name] = zodFor({ type: p.schema?.type, required: p.required, description: p.description });
116
- }
117
- }
118
- const bodySchema = op.requestBody?.content?.["application/json"]?.schema;
119
- if (bodySchema) {
120
- const required = new Set(bodySchema.required ?? []);
121
- for (const [name, prop] of Object.entries(bodySchema.properties ?? {})) {
122
- bodyPropNames.push(name);
123
- shape[name] = zodFor({ type: prop.type, required: required.has(name), description: prop.description });
124
- }
125
- }
126
- const name = toolNameFor(service, method, openapiPath);
127
- server.registerTool(name, {
128
- description: `[${service.name}] ${[op.summary, op.description].filter(Boolean).join(" — ")}`.slice(0, 1000),
129
- inputSchema: shape,
130
- }, async (args) => {
131
- let urlPath = openapiPath;
132
- for (const p of pathParamNames)
133
- urlPath = urlPath.replace(`{${p}}`, encodeURIComponent(String(args[p])));
134
- const url = new URL(service.baseUrl + urlPath);
135
- for (const p of queryParamNames) {
136
- const v = args[p];
137
- if (v !== undefined && v !== null)
138
- url.searchParams.set(p, String(v));
139
- }
140
- const headers = {};
141
- if (API_KEY)
142
- headers["x-api-key"] = API_KEY;
143
- const init = { method: method.toUpperCase(), headers };
144
- if (bodyPropNames.length) {
145
- const bodyObj = {};
146
- for (const p of bodyPropNames)
147
- if (args[p] !== undefined)
148
- bodyObj[p] = args[p];
149
- headers["content-type"] = "application/json";
150
- init.body = JSON.stringify(bodyObj);
151
- }
152
- const r = await doFetch(url.toString(), init);
153
- const text = await r.text();
154
- let body;
155
- try {
156
- body = JSON.parse(text);
157
- }
158
- catch {
159
- body = text;
160
- }
161
- return {
162
- isError: !r.ok,
163
- content: [{ type: "text", text: JSON.stringify(body, null, 2) }],
164
- };
165
- });
166
- registered++;
167
- }
168
- }
169
- return registered;
170
- }
171
- // --- Resources -----------------------------------------------------------
172
- // Reference material an agent can read without invoking a tool: the
173
- // platform-wide directory/llms.txt, and each service's live endpoint
174
- // catalog (the /v1/status we deliberately excluded from becoming a tool).
175
- function registerResources(server) {
176
- server.registerResource("platform-services", "wickedapi://platform/services", {
177
- title: "WickedAPI service directory",
178
- description: "Every live WickedAPI service — URL, category, one-line description, auth modes, and links to each one's own OpenAPI spec/docs/llms.txt.",
179
- mimeType: "application/json",
180
- }, async (uri) => {
181
- const r = await fetch(`${SHOWCASE_URL}/services.json`);
182
- if (!r.ok)
183
- throw new Error(`Failed to load service directory: HTTP ${r.status}`);
184
- return { contents: [{ uri: uri.toString(), mimeType: "application/json", text: await r.text() }] };
185
- });
186
- server.registerResource("platform-llms-txt", "wickedapi://platform/llms", {
187
- title: "WickedAPI llms.txt",
188
- description: "Terse, platform-wide summary for agents deciding what to call — auth modes, free vs. paid endpoints.",
189
- mimeType: "text/plain",
190
- }, async (uri) => {
191
- const r = await fetch(`${SHOWCASE_URL}/llms.txt`);
192
- if (!r.ok)
193
- throw new Error(`Failed to load llms.txt: HTTP ${r.status}`);
194
- return { contents: [{ uri: uri.toString(), mimeType: "text/plain", text: await r.text() }] };
195
- });
196
- for (const service of SERVICES) {
197
- server.registerResource(`${service.id}-status`, `wickedapi://${service.id}/status`, {
198
- title: `${service.name} — live catalog`,
199
- description: `Live endpoint catalog for ${service.name} (${service.baseUrl}), fetched fresh on every read.`,
200
- mimeType: "application/json",
201
- }, async (uri) => {
202
- const r = await fetch(`${service.baseUrl}/v1/status`);
203
- if (!r.ok)
204
- throw new Error(`Failed to load ${service.name} status: HTTP ${r.status}`);
205
- return { contents: [{ uri: uri.toString(), mimeType: "application/json", text: await r.text() }] };
206
- });
207
- }
208
- }
209
- // --- Prompts ---------------------------------------------------------------
210
- // The "compound recipes" from the WickedAPI cookbook, turned into one-click
211
- // prompts: each just tells the assistant which real tools to call and how
212
- // to read the results — no fabricated synthesis, every number cited comes
213
- // from an actual tool call made during the conversation.
214
- function registerPrompts(server) {
215
- server.registerPrompt("rug_check", {
216
- title: "Rug-check before buying",
217
- description: "Real on-chain risk signals for a token contract, plus market sentiment (and momentum, if it's a covered asset) before you decide whether to buy.",
218
- argsSchema: {
219
- address: z.string().describe("The token contract address (EVM) or mint address (Solana)."),
220
- chain: z.string().describe("ethereum, bsc, base, polygon, arbitrum, optimism, avalanche, fantom, zksync, linea, scroll, or solana."),
221
- symbol: z.string().optional().describe("If this token maps to a covered momentum-score asset (BTC, ETH, SOL, SUI, NVDA, IBIT, COIN, QQQ, TQQQ, DIA), pass it to also pull a real momentum verdict."),
222
- },
223
- }, ({ address, chain, symbol }) => ({
224
- description: "Rug-check workflow",
225
- messages: [
226
- {
227
- role: "user",
228
- content: {
229
- type: "text",
230
- text: `Before I consider buying this token, run a real risk check — don't guess or assume anything:\n\n` +
231
- `1. Call the \`token_risk\` tool with address="${address}" and chain="${chain}". Report the exact ` +
232
- `risk_score, risk_tier, and every real flag it returns (honeypot, mint/ownership authority, holder ` +
233
- `and LP concentration, buy/sell tax, trust-list status) — cite the actual values, don't summarize ` +
234
- `away the specifics or invent ones that weren't returned.\n` +
235
- `2. Call the \`sentiment\` tool for the current overall crypto market mood.\n` +
236
- (symbol
237
- ? `3. Call the \`momentum_score\` tool with symbol="${symbol}" and timeframe="1h" for a real technical read.\n`
238
- : "") +
239
- `\nThen give a plain verdict: the real risk signals found (listed, not paraphrased), current market ` +
240
- `mood, and — only if applicable — the momentum read. State clearly this is aggregated real signals, ` +
241
- `never a safety guarantee.`,
242
- },
243
- },
244
- ],
245
- }));
246
- server.registerPrompt("vet_dependency", {
247
- title: "Vet a DeFi dependency",
248
- description: "Protocol activity/treasury health plus an optional contract-level risk check, before you build on a DeFi protocol.",
249
- argsSchema: {
250
- slug: z.string().describe('The protocol\'s DefiLlama slug, e.g. "lido".'),
251
- token_address: z.string().optional().describe("The protocol's token contract address, if you also want a contract-level risk check."),
252
- chain: z.string().optional().describe("Chain for token_address (default ethereum)."),
253
- },
254
- }, ({ slug, token_address, chain }) => ({
255
- description: "Dependency-vetting workflow",
256
- messages: [
257
- {
258
- role: "user",
259
- content: {
260
- type: "text",
261
- text: `Before I integrate this protocol as a dependency, check its real health — don't guess:\n\n` +
262
- `1. Call the \`protocol_health_score\` tool with slug="${slug}". Report the components (GitHub ` +
263
- `activity, TVL trend, treasury) exactly as returned, including any that came back null (never ` +
264
- `filled in).\n` +
265
- (token_address
266
- ? `2. Call the \`token_risk\` tool with address="${token_address}" and chain="${chain || "ethereum"}" ` +
267
- `for a contract-level check on its token.\n`
268
- : "") +
269
- `\nSummarize: is this protocol actively maintained, is its TVL trend healthy, and (if checked) does ` +
270
- `its token carry any real contract-level risk flags. Cite the real numbers.`,
271
- },
272
- },
273
- ],
274
- }));
275
- server.registerPrompt("morning_briefing", {
276
- title: "Morning briefing",
277
- description: "One daily digest from macro calendar, earnings calendar, sentiment, and a momentum verdict — all free or a fraction of a cent.",
278
- argsSchema: {
279
- symbol: z.string().optional().describe("Asset for the momentum verdict (default BTC). One of BTC, ETH, SOL, SUI, NVDA, IBIT, COIN, QQQ, TQQQ, DIA."),
280
- },
281
- }, ({ symbol }) => {
282
- const sym = symbol || "BTC";
283
- return {
284
- description: "Morning briefing workflow",
285
- messages: [
286
- {
287
- role: "user",
288
- content: {
289
- type: "text",
290
- text: `Compile my morning briefing from real, current data — call each tool, don't estimate:\n\n` +
291
- `1. \`macro_calendar\` with days=7 — upcoming FOMC/CPI/jobs events.\n` +
292
- `2. \`earnings_calendar\` with days=3 — upcoming earnings reports.\n` +
293
- `3. \`sentiment\` — current crypto Fear & Greed reading.\n` +
294
- `4. \`momentum_score\` with symbol="${sym}" and timeframe="1D" — today's technical verdict.\n\n` +
295
- `Then write one concise digest combining all four, citing the real values from each call.`,
296
- },
297
- },
298
- ],
299
- };
300
- });
301
- }
302
- export const SERVER_VERSION = "0.4.0";
303
- export async function loadWorld() {
304
- const doFetch = await buildFetcher();
305
- const specs = [];
306
- for (const service of SERVICES) {
307
- const doc = await loadServiceSpec(service);
308
- if (doc)
309
- specs.push({ service, doc });
310
- }
311
- if (specs.length === 0) {
312
- throw new Error("No service specs loaded - check network access and WICKEDAPI_*_URL overrides.");
313
- }
314
- return { doFetch, specs };
315
- }
316
- export function buildServer(world, hooks) {
317
- const server = new McpServer({ name: "wickedapi", version: SERVER_VERSION });
318
- if (hooks)
319
- instrument(server, hooks);
320
- let totalTools = 0;
321
- const loaded = [];
322
- for (const { service, doc } of world.specs) {
323
- totalTools += registerServiceTools(server, service, doc, world.doFetch);
324
- loaded.push(service.name);
325
- }
326
- // Reputation, Registry, Identity, Sanity, Memory and Paywall use hand-written
327
- // tools (non-/v1 paths, wallet-signature auth), registered unconditionally.
328
- totalTools += registerSuiteTools(server);
329
- loaded.push("Reputation, Registry, Identity, Sanity, Memory, Paywall");
330
- registerResources(server);
331
- registerPrompts(server);
332
- registerSuitePrompts(server);
333
- return {
334
- server,
335
- summary: `${totalTools} tools across ${loaded.length} group(s) (${loaded.join(", ")}), ${2 + SERVICES.length} resources, 7 prompts`,
336
- };
337
- }