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