wickedapi-mcp 0.1.0 → 0.3.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.
Files changed (4) hide show
  1. package/README.md +109 -67
  2. package/dist/index.js +295 -54
  3. package/dist/suite.js +395 -0
  4. package/package.json +55 -47
package/README.md CHANGED
@@ -1,67 +1,109 @@
1
- # WickedAPI MCP Server
2
-
3
- MCP server for [WickedAPI](https://api.wickedapi.com) — an agent-ready trading & market-data API covering crypto/stock prices, OHLCV, technical indicators (ATR, Fair Value Gaps, ICT liquidity levels), perp funding/OI, order books, Kalshi prediction markets, DeFi TVL, volatility, a verified macro calendar, an earnings calendar, and an AI momentum score.
4
-
5
- Tools are generated automatically from the live [`openapi.json`](https://api.wickedapi.com/openapi.json), so the tool list always matches what's actually deployed.
6
-
7
- ## No signup required
8
-
9
- Every tool call hits a real `/v1/*` endpoint that's gated by [x402](https://x402.org) (HTTP-native pay-per-call, USDC on Base) with an API-key fallback:
10
-
11
- - **Zero config** — calls without credentials still work; you'll get back the real `402` payment challenge (price, network, `payTo`) as the tool result.
12
- - **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 if you have one.
14
-
15
- ## Install
16
-
17
- ```json
18
- {
19
- "mcpServers": {
20
- "wickedapi": {
21
- "command": "npx",
22
- "args": ["-y", "wickedapi-mcp"],
23
- "env": {
24
- "WICKEDAPI_API_KEY": "your-key-here"
25
- }
26
- }
27
- }
28
- }
29
- ```
30
-
31
- Or for autonomous pay-per-call instead of a key:
32
-
33
- ```json
34
- {
35
- "mcpServers": {
36
- "wickedapi": {
37
- "command": "npx",
38
- "args": ["-y", "wickedapi-mcp"],
39
- "env": {
40
- "WICKEDAPI_PAYER_PRIVATE_KEY": "0xyour-base-wallet-private-key"
41
- }
42
- }
43
- }
44
- }
45
- ```
46
-
47
- ## Environment variables
48
-
49
- | Var | Required | Notes |
50
- | --- | --- | --- |
51
- | `WICKEDAPI_API_KEY` | no | Free, rate-limited access if you have a partner key. |
52
- | `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`). |
54
-
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.
56
-
57
- ## Local development
58
-
59
- ```bash
60
- npm install
61
- npm run build
62
- node dist/index.js
63
- ```
64
-
65
- ## What this is not
66
-
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 scoring live in the API itself.
1
+ # WickedAPI MCP Server
2
+
3
+ MCP server for the whole [Wicked](https://wickedapi.com) suite — trading data, DeFi protocol health, token safety, brokerage sync, agent identity, reputation, a tool-reliability registry, hallucination checks, persistent agent memory, and x402 paywalls, in one server.
4
+
5
+ **70 tools** across 10 live services. The first four are generated automatically from each service's own live `openapi.json`, so the list always matches what's deployed; the rest are hand-written because they use wallet signatures and non-`/v1` routes:
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
+ | [Wicked Reputation](https://stake.wickedapi.com) | 5 (`reputation_*`) | free, no key |
14
+ | [Wicked Registry](https://registry.wickedapi.com) | 6 (`registry_*`) | search/detail: key or x402; featured/badge/report free; register needs your signature |
15
+ | [Wicked Identity](https://verify.wickedapi.com) | 6 (`identity_*`) | status: key or x402; the rest need your wallet signature over a nonce |
16
+ | [Wicked Sanity](https://sanity.wickedapi.com) | 2 (`sanity_check`, `sanity_check_batch`) | key or x402 |
17
+ | [Wicked Memory](https://memory.wickedapi.com) | 8 (`memory_prepare`, `memory_store`, `memory_search`, …) | wallet signature on every call; only search is metered |
18
+ | [x402 Paywall](https://paywall.wickedapi.com) | 8 (`paywall_*`) | `paywall_signup` is open; the rest need the tenant key |
19
+
20
+ 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.
21
+
22
+ These tools never sign anything or hold a key. Identity, Registry registration and Memory take a signature you produce with your own wallet (`identity_get_nonce` / `memory_prepare` return the exact message to sign). Calls that need x402 payment return the real `402` challenge (decoded under `payment_required` for x402 v2) rather than auto-paying — `WICKEDAPI_PAYER_PRIVATE_KEY` auto-pay applies to the Trading, Protocol Health, Token Risk and Broker Sync tools.
23
+
24
+ ## No signup required
25
+
26
+ 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:
27
+
28
+ - **Zero config** — calls without credentials still work; paid endpoints return the real `402` payment challenge (price, network, `payTo`) as the tool result.
29
+ - **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.
30
+ - **Partner API key** — set `WICKEDAPI_API_KEY` for free, rate-limited access across all four services (same key works everywhere).
31
+
32
+ ## Install
33
+
34
+ ```json
35
+ {
36
+ "mcpServers": {
37
+ "wickedapi": {
38
+ "command": "npx",
39
+ "args": ["-y", "wickedapi-mcp"],
40
+ "env": {
41
+ "WICKEDAPI_API_KEY": "your-key-here"
42
+ }
43
+ }
44
+ }
45
+ }
46
+ ```
47
+
48
+ Or for autonomous pay-per-call instead of a key:
49
+
50
+ ```json
51
+ {
52
+ "mcpServers": {
53
+ "wickedapi": {
54
+ "command": "npx",
55
+ "args": ["-y", "wickedapi-mcp"],
56
+ "env": {
57
+ "WICKEDAPI_PAYER_PRIVATE_KEY": "0xyour-base-wallet-private-key"
58
+ }
59
+ }
60
+ }
61
+ }
62
+ ```
63
+
64
+ 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.
65
+
66
+ ## Resources
67
+
68
+ Read without calling a tool — useful for a client that surfaces resources in its UI:
69
+
70
+ - `wickedapi://platform/services` — the full service directory (JSON)
71
+ - `wickedapi://platform/llms` — the platform's `llms.txt`
72
+ - `wickedapi://trading/status`, `wickedapi://protocol_health/status`, `wickedapi://token_risk/status`, `wickedapi://broker_sync/status` — each service's live endpoint catalog
73
+
74
+ ## Prompts
75
+
76
+ One-click workflows that chain real tool calls (Claude Desktop and other prompt-aware clients surface these directly):
77
+
78
+ - **`rug_check`** (`address`, `chain`, optional `symbol`) — token risk check + market sentiment + optional momentum read, before buying.
79
+ - **`vet_dependency`** (`slug`, optional `token_address`, `chain`) — protocol health + optional contract-level risk check, before building on a DeFi protocol.
80
+ - **`morning_briefing`** (optional `symbol`) — macro calendar + earnings calendar + sentiment + momentum verdict, as one digest.
81
+
82
+ ## Environment variables
83
+
84
+ | Var | Required | Notes |
85
+ | --- | --- | --- |
86
+ | `WICKEDAPI_API_KEY` | no | Free, rate-limited access across all four services (same key works everywhere). |
87
+ | `WICKEDAPI_PAYER_PRIVATE_KEY` | no | Base-mainnet wallet private key. If set, every call auto-pays the x402 challenge in USDC — no key needed. |
88
+ | `WICKEDAPI_BASE_URL` | no | Override the Trading Data API base (default `https://api.wickedapi.com`). |
89
+ | `WICKEDAPI_PROTOCOL_HEALTH_URL` | no | Override the Protocol Health Oracle base (default `https://protocol-health-oracle.wickedapi.com`). |
90
+ | `WICKEDAPI_TOKEN_RISK_URL` | no | Override the Token Risk Oracle base (default `https://token-risk-oracle.wickedapi.com`). |
91
+ | `WICKEDAPI_BROKER_SYNC_URL` | no | Override the Broker Sync API base (default `https://broker-sync-api.wickedapi.com`). |
92
+ | `REGISTRY_API_KEY` / `IDENTITY_API_KEY` / `SANITY_API_KEY` / `MEMORY_API_KEY` | no | Free-tier keys for the paid calls of Registry, Identity, Sanity and Memory search. Omit to get the x402 challenge instead. |
93
+ | `PAYWALL_API_KEY` | for `paywall_*` | Tenant Bearer key returned once by `paywall_signup`. |
94
+ | `REPUTATION_BASE_URL` / `REGISTRY_BASE_URL` / `IDENTITY_BASE_URL` / `SANITY_BASE_URL` / `MEMORY_BASE_URL` / `PAYWALL_BASE_URL` | no | Override the matching service base (defaults are the `*.wickedapi.com` hosts). |
95
+ | `WICKEDAPI_SHOWCASE_URL` | no | Override the platform directory base used by resources (default `https://wickedapi.com`). |
96
+
97
+ 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.
98
+
99
+ ## Local development
100
+
101
+ ```bash
102
+ npm install
103
+ npm run build
104
+ node dist/index.js
105
+ ```
106
+
107
+ ## What this is not
108
+
109
+ This server doesn't reimplement any data logic — it's a thin bridge from MCP tool calls to the live Wicked HTTP endpoints across all ten services. All the actual market data, indicators, scoring, and risk signals live in the APIs themselves.
package/dist/index.js CHANGED
@@ -2,21 +2,52 @@
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
- const BASE_URL = (process.env.WICKEDAPI_BASE_URL || "https://api.wickedapi.com").replace(/\/$/, "");
5
+ import { registerSuiteTools } from "./suite.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");
6
16
  const API_KEY = process.env.WICKEDAPI_API_KEY;
7
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
+ };
8
33
  // If a funded Base-mainnet wallet key is provided, every call auto-pays the
9
34
  // x402 402 challenge in USDC — no API key or signup needed at all. Loaded
10
35
  // lazily so a plain API-key setup never has to pull in viem/x402-fetch.
11
36
  async function buildFetcher() {
12
37
  if (!PAYER_KEY)
13
38
  return fetch;
14
- const [{ createWalletClient, http }, { privateKeyToAccount }, { base }, { wrapFetchWithPayment }] = await Promise.all([
15
- import("viem"),
16
- import("viem/accounts"),
17
- import("viem/chains"),
18
- import("x402-fetch"),
19
- ]);
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;
20
51
  const account = privateKeyToAccount(PAYER_KEY);
21
52
  const wallet = createWalletClient({ account, chain: base, transport: http() });
22
53
  console.error(`[wickedapi-mcp] x402 auto-pay enabled — wallet ${account.address}`);
@@ -25,65 +56,275 @@ async function buildFetcher() {
25
56
  // wallet client the same way — cast rather than pull in publicActions.
26
57
  return wrapFetchWithPayment(fetch, wallet);
27
58
  }
28
- function toolNameFromPath(path) {
29
- return path.replace(/^\/v1\//, "").replace(/-/g, "_");
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}`;
30
73
  }
31
- function zodForParam(p) {
32
- const type = p.schema?.type;
33
- const base = type === "number" ? z.coerce.number() : type === "boolean" ? z.coerce.boolean() : z.string();
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();
34
80
  const described = p.description ? base.describe(p.description) : base;
35
81
  return p.required ? described : described.optional();
36
82
  }
37
- async function main() {
38
- const specRes = await fetch(`${BASE_URL}/openapi.json`);
39
- if (!specRes.ok) {
40
- throw new Error(`Failed to load ${BASE_URL}/openapi.json: HTTP ${specRes.status}`);
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());
41
89
  }
42
- const doc = (await specRes.json());
43
- const doFetch = await buildFetcher();
44
- const server = new McpServer({ name: "wickedapi", version: "0.1.0" });
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) {
45
97
  let registered = 0;
46
- for (const [path, methods] of Object.entries(doc.paths)) {
47
- const op = methods.get;
48
- if (!op || !path.startsWith("/v1/") || path === "/v1/status")
49
- continue;
50
- const shape = {};
51
- for (const p of op.parameters ?? []) {
52
- if (p.in !== "query")
53
- continue;
54
- shape[p.name] = zodForParam(p);
55
- }
56
- server.registerTool(toolNameFromPath(path), {
57
- description: [op.summary, op.description].filter(Boolean).join(" — ").slice(0, 1000),
58
- inputSchema: shape,
59
- }, async (args) => {
60
- const url = new URL(BASE_URL + path);
61
- for (const [k, v] of Object.entries(args ?? {})) {
62
- if (v !== undefined && v !== null)
63
- url.searchParams.set(k, String(v));
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
+ }
64
117
  }
65
- const headers = {};
66
- if (API_KEY)
67
- headers["x-api-key"] = API_KEY;
68
- const r = await doFetch(url.toString(), { headers });
69
- const text = await r.text();
70
- let body;
71
- try {
72
- body = JSON.parse(text);
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
+ }
73
125
  }
74
- catch {
75
- body = text;
76
- }
77
- return {
78
- isError: !r.ok,
79
- content: [{ type: "text", text: JSON.stringify(body, null, 2) }],
80
- };
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() }] };
81
206
  });
82
- registered++;
83
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
+ async function main() {
303
+ const server = new McpServer({ name: "wickedapi", version: "0.3.0" });
304
+ const doFetch = await buildFetcher();
305
+ let totalTools = 0;
306
+ const loaded = [];
307
+ for (const service of SERVICES) {
308
+ const doc = await loadServiceSpec(service);
309
+ if (!doc)
310
+ continue;
311
+ totalTools += registerServiceTools(server, service, doc, doFetch);
312
+ loaded.push(service.name);
313
+ }
314
+ if (totalTools === 0) {
315
+ throw new Error("No service specs loaded — check network access and WICKEDAPI_*_URL overrides.");
316
+ }
317
+ // Reputation, Registry, Identity, Sanity, Memory and Paywall use hand-written
318
+ // tools (non-/v1 paths, wallet-signature auth) — registered unconditionally.
319
+ const suiteTools = registerSuiteTools(server);
320
+ totalTools += suiteTools;
321
+ loaded.push("Reputation, Registry, Identity, Sanity, Memory, Paywall");
322
+ registerResources(server);
323
+ registerPrompts(server);
84
324
  const transport = new StdioServerTransport();
85
325
  await server.connect(transport);
86
- console.error(`[wickedapi-mcp] ready — ${registered} tools loaded from ${BASE_URL}/openapi.json`);
326
+ console.error(`[wickedapi-mcp] ready — ${totalTools} tools across ${loaded.length} service(s) (${loaded.join(", ")}), ` +
327
+ `${2 + SERVICES.length} resources, 3 prompts.`);
87
328
  }
88
329
  main().catch((err) => {
89
330
  console.error("[wickedapi-mcp] fatal:", err);
package/dist/suite.js ADDED
@@ -0,0 +1,395 @@
1
+ // Hand-written tools for the Wicked trust/memory/payments services whose HTTP
2
+ // surface isn't the `/v1/*` OpenAPI shape the generic loader in index.ts
3
+ // understands: Reputation, Registry, Identity, Sanity, Memory and the x402
4
+ // Paywall. Behaviour mirrors the Python wicked-mcp server (tool names, args,
5
+ // and the `http_status` / `payment_required` envelope) so agents see the same
6
+ // surface whether they use the remote server or this npm package.
7
+ //
8
+ // Nothing here signs anything or holds a wallet key: Identity, Registry
9
+ // registration and Memory take a signature the caller produced themselves.
10
+ import { createHash, randomBytes } from "node:crypto";
11
+ import { z } from "zod";
12
+ function envUrl(name, fallback) {
13
+ return (process.env[name] || fallback).replace(/\/$/, "");
14
+ }
15
+ const REPUTATION_URL = envUrl("REPUTATION_BASE_URL", "https://stake.wickedapi.com");
16
+ const REGISTRY_URL = envUrl("REGISTRY_BASE_URL", "https://registry.wickedapi.com");
17
+ const IDENTITY_URL = envUrl("IDENTITY_BASE_URL", "https://verify.wickedapi.com");
18
+ const SANITY_URL = envUrl("SANITY_BASE_URL", "https://sanity.wickedapi.com");
19
+ const MEMORY_URL = envUrl("MEMORY_BASE_URL", "https://memory.wickedapi.com");
20
+ const PAYWALL_URL = envUrl("PAYWALL_BASE_URL", "https://paywall.wickedapi.com");
21
+ const REGISTRY_API_KEY = process.env.REGISTRY_API_KEY || "";
22
+ const IDENTITY_API_KEY = process.env.IDENTITY_API_KEY || "";
23
+ const SANITY_API_KEY = process.env.SANITY_API_KEY || "";
24
+ const MEMORY_API_KEY = process.env.MEMORY_API_KEY || "";
25
+ const PAYWALL_API_KEY = process.env.PAYWALL_API_KEY || "";
26
+ const DEFAULT_TIMEOUT_MS = 20_000;
27
+ const SANITY_TIMEOUT_MS = 90_000; // open mode runs a live web search per claim
28
+ // x402 v2 services put the payment instructions in a base64 PAYMENT-REQUIRED
29
+ // header instead of the JSON body; decode it so a 402 is useful either way.
30
+ function extractPaymentRequired(res) {
31
+ const header = res.headers.get("payment-required");
32
+ if (!header)
33
+ return undefined;
34
+ try {
35
+ return JSON.parse(Buffer.from(header, "base64").toString("utf8"));
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ }
41
+ async function finalize(res) {
42
+ const text = await res.text();
43
+ let parsed;
44
+ try {
45
+ parsed = JSON.parse(text);
46
+ }
47
+ catch {
48
+ parsed = { raw_body: text };
49
+ }
50
+ const body = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { data: parsed };
51
+ body.http_status = res.status;
52
+ const pr = extractPaymentRequired(res);
53
+ if (pr !== undefined)
54
+ body.payment_required = pr;
55
+ const retryAfter = res.headers.get("retry-after");
56
+ if (res.status === 429 && retryAfter)
57
+ body.retry_after = retryAfter;
58
+ return body;
59
+ }
60
+ function asResult(body) {
61
+ const status = Number(body.http_status ?? 200);
62
+ // 4xx/5xx are returned (not thrown) because 402/404/429 bodies are data the
63
+ // calling agent needs; flag them as errors without hiding the payload.
64
+ return { isError: status >= 500, content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
65
+ }
66
+ function clean(obj) {
67
+ return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined && v !== null));
68
+ }
69
+ async function call(method, base, path, opts = {}) {
70
+ const url = new URL(base + path);
71
+ for (const [k, v] of Object.entries(clean(opts.query ?? {})))
72
+ url.searchParams.set(k, String(v));
73
+ const headers = { ...(opts.headers ?? {}) };
74
+ const init = { method, headers, signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS) };
75
+ if (opts.json) {
76
+ headers["content-type"] = "application/json";
77
+ init.body = JSON.stringify(clean(opts.json));
78
+ }
79
+ try {
80
+ return await finalize(await fetch(url.toString(), init));
81
+ }
82
+ catch (err) {
83
+ return { error: err.message, source: "mcp", http_status: 502 };
84
+ }
85
+ }
86
+ const keyHeader = (key) => (key ? { "x-api-key": key } : {});
87
+ const bearer = (key) => (key ? { authorization: `Bearer ${key}` } : {});
88
+ const addr = (wallet) => encodeURIComponent(wallet.toLowerCase());
89
+ // --- Wicked Memory request signing --------------------------------------
90
+ // Every /memories* call carries an EIP-191 signature over a message binding
91
+ // the wallet to the exact method, path, body, timestamp and nonce. The path
92
+ // and body bytes built here are identical for memory_prepare and the call
93
+ // itself, so pass the same arguments to both.
94
+ const MEMORY_OPS = ["store", "search", "get", "update", "history", "delete", "deletions"];
95
+ const EMPTY_SHA256 = createHash("sha256").update("").digest("hex");
96
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
97
+ function memoryId(p) {
98
+ const id = String(p.memory_id ?? "");
99
+ if (!UUID_RE.test(id))
100
+ throw new Error("memory_id must be a UUID");
101
+ return id.toLowerCase();
102
+ }
103
+ // Matches Python's urlencode(quote_via=quote): the signed path must be
104
+ // byte-identical to the one the Python server signs, and encodeURIComponent
105
+ // differs on ! ' ( ) *.
106
+ function pyQuote(s) {
107
+ return encodeURIComponent(s).replace(/[!'()*]/g, (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase());
108
+ }
109
+ function queryString(q) {
110
+ return Object.entries(q)
111
+ .map(([k, v]) => `${pyQuote(k)}=${pyQuote(v)}`)
112
+ .join("&");
113
+ }
114
+ function memoryBuild(operation, params) {
115
+ const p = clean(params ?? {});
116
+ if (!MEMORY_OPS.includes(operation)) {
117
+ throw new Error(`unknown operation '${operation}'; expected one of ${[...MEMORY_OPS].sort().join(", ")}`);
118
+ }
119
+ const body = (keys) => JSON.stringify(Object.fromEntries(keys.filter((k) => k in p).map((k) => [k, p[k]])));
120
+ switch (operation) {
121
+ case "store":
122
+ if (!("content" in p))
123
+ throw new Error("store requires content");
124
+ return { method: "POST", path: "/memories", body: body(["content", "tags", "metadata", "source"]) };
125
+ case "search": {
126
+ if (!("q" in p))
127
+ throw new Error("search requires q");
128
+ const q = { q: String(p.q) };
129
+ for (const k of ["limit", "created_after", "created_before", "as_of"])
130
+ if (k in p)
131
+ q[k] = String(p[k]);
132
+ if ("tags" in p)
133
+ q.tags = Array.isArray(p.tags) ? p.tags.join(",") : String(p.tags);
134
+ if ("include_superseded" in p)
135
+ q.include_superseded = p.include_superseded ? "true" : "false";
136
+ return { method: "GET", path: `/memories/search?${queryString(q)}` };
137
+ }
138
+ case "get":
139
+ return { method: "GET", path: `/memories/${memoryId(p)}` };
140
+ case "update":
141
+ return { method: "PATCH", path: `/memories/${memoryId(p)}`, body: body(["content", "tags", "metadata", "source"]) };
142
+ case "history":
143
+ return { method: "GET", path: `/memories/${memoryId(p)}/history` };
144
+ case "delete": {
145
+ // scope is always explicit so prepare (may omit it) and delete (default "chain") sign the same path.
146
+ const q = { scope: String(p.scope ?? "chain") };
147
+ if ("reason" in p)
148
+ q.reason = String(p.reason);
149
+ return { method: "DELETE", path: `/memories/${memoryId(p)}?${queryString(q)}` };
150
+ }
151
+ case "deletions":
152
+ return { method: "GET", path: "/memory-deletions" };
153
+ }
154
+ }
155
+ function memoryMessage(wallet, method, path, timestamp, nonce, body) {
156
+ const sha = body ? createHash("sha256").update(body, "utf8").digest("hex") : EMPTY_SHA256;
157
+ return [
158
+ "Wicked Memory auth",
159
+ `wallet: ${wallet.toLowerCase()}`,
160
+ `method: ${method.toUpperCase()}`,
161
+ `path: ${path}`,
162
+ `timestamp: ${timestamp}`,
163
+ `nonce: ${nonce}`,
164
+ `body-sha256: ${sha}`,
165
+ ].join("\n");
166
+ }
167
+ async function memoryCall(operation, wallet, params, sig, paymentSignature) {
168
+ let built;
169
+ try {
170
+ built = memoryBuild(operation, params);
171
+ }
172
+ catch (err) {
173
+ return { error: err.message, source: "mcp", http_status: 400 };
174
+ }
175
+ const headers = {
176
+ "x-wallet-address": wallet,
177
+ "x-wallet-timestamp": sig.timestamp,
178
+ "x-wallet-nonce": sig.nonce,
179
+ "x-wallet-signature": sig.signature,
180
+ };
181
+ if (built.body !== undefined)
182
+ headers["content-type"] = "application/json";
183
+ if (operation === "search") {
184
+ if (MEMORY_API_KEY)
185
+ headers["x-api-key"] = MEMORY_API_KEY;
186
+ if (paymentSignature)
187
+ headers["PAYMENT-SIGNATURE"] = paymentSignature;
188
+ }
189
+ try {
190
+ const res = await fetch(MEMORY_URL + built.path, {
191
+ method: built.method,
192
+ headers,
193
+ body: built.body,
194
+ signal: AbortSignal.timeout(30_000),
195
+ });
196
+ return await finalize(res);
197
+ }
198
+ catch (err) {
199
+ return { error: err.message, source: "mcp", http_status: 502 };
200
+ }
201
+ }
202
+ // --- Registration ---------------------------------------------------------
203
+ const sigShape = {
204
+ timestamp: z.string().describe("From memory_prepare."),
205
+ nonce: z.string().describe("From memory_prepare (single use)."),
206
+ signature: z.string().describe("Your wallet's signature over memory_prepare's message_to_sign."),
207
+ };
208
+ export function registerSuiteTools(server) {
209
+ let n = 0;
210
+ const tool = (name, description, shape, run) => {
211
+ server.registerTool(name, { description, inputSchema: shape }, async (args) => asResult(await run(args)));
212
+ n++;
213
+ };
214
+ const wallet = z.string().describe("The 0x wallet address.");
215
+ // ---- Wicked Reputation (all public, free) ----
216
+ tool("reputation_status", "[Wicked Reputation] An agent's current reputation: tier, active/unbonding stake, score, slash count, founding-member badge. http_status 404 means the wallet never registered — treat as tier 'unranked'.", { wallet }, ({ wallet }) => call("GET", REPUTATION_URL, `/agents/${addr(wallet)}/status`));
217
+ tool("reputation_statement", "[Wicked Reputation] An agent's full position plus real event ledger (stakes, unstakes, withdrawals, slashes). Does not include WickedAPI call volume or fees — see the `note` field.", { wallet }, ({ wallet }) => call("GET", REPUTATION_URL, `/agents/${addr(wallet)}/statement`));
218
+ tool("reputation_query", "[Wicked Reputation] Search/rank registered agents by tier, stake, reputation or slash history. Every filter is a typed query param.", {
219
+ tier: z.string().optional().describe('Exact tier name, e.g. "gold".'),
220
+ min_stake: z.coerce.number().optional().describe("Minimum active stake (USDC)."),
221
+ max_stake: z.coerce.number().optional().describe("Maximum active stake (USDC)."),
222
+ min_reputation: z.coerce.number().optional().describe("Minimum reputation score."),
223
+ founding_member: z.coerce.boolean().optional().describe("True to only show founding members."),
224
+ min_slash_count: z.coerce.number().optional().describe("Minimum slash events (1 finds agents ever slashed)."),
225
+ sort: z.string().optional().describe('stake_amount | reputation_score | slash_count | registered_at, prefixed "-" (desc, default) or "+" (asc).'),
226
+ limit: z.coerce.number().optional().describe("1-200, default 20."),
227
+ }, (a) => call("GET", REPUTATION_URL, "/agents/query", { query: { sort: "-stake_amount", limit: 20, ...clean(a) } }));
228
+ tool("reputation_tiers", "[Wicked Reputation] Live tier thresholds and benefits (min stake, rate-limit multiplier, fee discount). Read at call time — thresholds are admin-configurable.", {}, () => call("GET", REPUTATION_URL, "/tiers"));
229
+ tool("reputation_transparency", "[Wicked Reputation] Live transparency numbers: treasury address, custody model, unbonding cooldown, total stake, agents, slash events, founding-slot counts.", {}, () => call("GET", REPUTATION_URL, "/transparency"));
230
+ // ---- Wicked Registry ----
231
+ tool("registry_search_tools", "[Wicked Registry] Search x402/MCP tools by real reliability data (uptime, latency, schema conformance). Works via x402 (402 carries payment instructions) or a free-tier REGISTRY_API_KEY.", {
232
+ category: z.string().optional().describe('e.g. "data", "crypto".'),
233
+ protocol: z.string().optional().describe('"x402", "mcp" or "rest".'),
234
+ min_score: z.coerce.number().optional().describe("Minimum composite score 0-100; unchecked tools are excluded once set."),
235
+ sort: z.string().optional().describe('"score" (default), "latency" or "recency".'),
236
+ limit: z.coerce.number().optional().describe("1-200, default 50."),
237
+ }, (a) => call("GET", REGISTRY_URL, "/tools", { headers: keyHeader(REGISTRY_API_KEY), query: { sort: "score", limit: 50, ...clean(a) } }));
238
+ tool("registry_tool_detail", "[Wicked Registry] One tool's full score breakdown, component history and recent real health checks. x402 or REGISTRY_API_KEY.", { tool_id: z.string().describe("Registry id from registry_search_tools / registry_featured_tools.") }, ({ tool_id }) => call("GET", REGISTRY_URL, `/tools/${encodeURIComponent(tool_id)}`, { headers: keyHeader(REGISTRY_API_KEY) }));
239
+ tool("registry_featured_tools", "[Wicked Registry] Top scored active tools. Public and free.", {}, () => call("GET", REGISTRY_URL, "/featured-tools"));
240
+ tool("registry_tool_badge", "[Wicked Registry] A tool's current composite reliability score. Public and free.", { tool_id: z.string().describe("The tool's Wicked Registry id.") }, ({ tool_id }) => call("GET", REGISTRY_URL, `/tools/${encodeURIComponent(tool_id)}/badge`));
241
+ tool("registry_report_tool", "[Wicked Registry] File a complaint about a tool's reliability or behaviour. Public, rate-limited; reviewed manually, never auto-suspends.", {
242
+ tool_id: z.string().describe("The tool's Wicked Registry id."),
243
+ reporter: z.string().describe("Your wallet address or another best-effort identifier."),
244
+ reason: z.string().describe('"downtime", "incorrect_response", "scam_or_abuse", "schema_violation" or "other".'),
245
+ evidence: z.string().optional().describe("Optional note or URL."),
246
+ }, ({ tool_id, reporter, reason, evidence }) => call("POST", REGISTRY_URL, `/tools/${encodeURIComponent(tool_id)}/report`, { json: { reporter, reason, evidence } }));
247
+ tool("registry_register_tool", '[Wicked Registry] Register a tool you own so it gets real synthetic checks and a live score. Needs YOUR signature over "Wicked Registry — Tool Registration\\nname: {name}\\nendpoint: {endpoint_url}\\ntimestamp: {timestamp}" from owner_wallet; this tool never signs.', {
248
+ name: z.string(),
249
+ endpoint_url: z.string().describe("Live, publicly reachable endpoint."),
250
+ protocol: z.string().describe('"x402", "mcp" or "rest".'),
251
+ category: z.string(),
252
+ description: z.string(),
253
+ owner_wallet: z.string().describe("0x wallet that signed the registration message."),
254
+ signature: z.string().describe("0x-prefixed ECDSA signature over the registration message."),
255
+ timestamp: z.string().describe("ISO 8601 timestamp used in the signed message (within 10 minutes of the call)."),
256
+ schema_url: z.string().optional().describe("Optional JSON Schema URL enabling the schema-conformance score."),
257
+ }, (a) => call("POST", REGISTRY_URL, "/tools/register", {
258
+ json: {
259
+ name: a.name,
260
+ endpointUrl: a.endpoint_url,
261
+ protocol: a.protocol,
262
+ category: a.category,
263
+ description: a.description,
264
+ ownerWallet: a.owner_wallet,
265
+ signature: a.signature,
266
+ timestamp: a.timestamp,
267
+ schemaUrl: a.schema_url,
268
+ },
269
+ }));
270
+ // ---- Wicked Identity (reverse-CAPTCHA / Know-Your-Agent) ----
271
+ const idSig = {
272
+ wallet: z.string().describe("Your agent's 0x wallet address on Base."),
273
+ nonce: z.string().describe("Nonce from a FRESH identity_get_nonce call."),
274
+ signature: z.string().describe("0x-prefixed ECDSA signature over that nonce's message."),
275
+ };
276
+ tool("identity_get_nonce", "[Wicked Identity] Step 1: a one-time nonce + message to sign. Every signed identity call needs a fresh nonce (single-use, short TTL).", { wallet: idSig.wallet }, ({ wallet }) => call("GET", IDENTITY_URL, `/agents/${addr(wallet)}/nonce`));
277
+ tool("identity_register", "[Wicked Identity] Register your wallet (no stake required). Needs your signature over identity_get_nonce's message; this tool never signs.", idSig, ({ wallet, nonce, signature }) => call("POST", IDENTITY_URL, "/agents/register", { json: { wallet_address: wallet, nonce, signature } }));
278
+ tool("identity_get_challenge", "[Wicked Identity] Request a real time-boxed liveness challenge for a registered wallet (constrained-generation task, ~12s). Answer via identity_submit_response before `expires_at`. Needs a fresh signed nonce.", idSig, ({ wallet, nonce, signature }) => call("POST", IDENTITY_URL, "/verify/challenge", { json: { wallet_address: wallet, nonce, signature } }));
279
+ tool("identity_submit_response", "[Wicked Identity] Submit your answer to a challenge; scored pass / fail / inconclusive. A pass returns a short-lived signed assertion_token (JWT) verifiable via identity_jwks. Needs a DIFFERENT fresh nonce than identity_get_challenge used.", {
280
+ ...idSig,
281
+ challenge_id: z.string().describe("The challenge_id from identity_get_challenge."),
282
+ response_text: z.string().describe("Your answer exactly as the instructions specify."),
283
+ }, ({ wallet, nonce, signature, challenge_id, response_text }) => call("POST", IDENTITY_URL, "/verify/response", { json: { wallet_address: wallet, nonce, signature, challenge_id, response_text } }));
284
+ tool("identity_status", "[Wicked Identity] Does a wallet currently hold a valid, unexpired assertion? No signature needed. x402 or IDENTITY_API_KEY.", { wallet }, ({ wallet }) => call("GET", IDENTITY_URL, `/verify/status/${addr(wallet)}`, { headers: keyHeader(IDENTITY_API_KEY) }));
285
+ tool("identity_jwks", "[Wicked Identity] Public RS256 keys to verify an assertion_token locally. Free.", {}, () => call("GET", IDENTITY_URL, "/.well-known/jwks.json"));
286
+ // ---- Wicked Sanity (hallucination / eval check) ----
287
+ const contextShape = z
288
+ .union([z.string(), z.array(z.string())])
289
+ .optional()
290
+ .describe('Source text the claim(s) must be faithful to. Required for mode "grounded".');
291
+ const modeShape = z.string().optional().describe('"grounded" (default; check against context) or "open" (check against live web evidence — consistency with the web, not objective truth).');
292
+ tool("sanity_check", '[Wicked Sanity] Verify one claim before acting on it. Verdicts: supported | contradicted | unsupported | insufficient_evidence (see `data`). confidence_score is raw consistency (a "contradicted" verdict scores near 0). Policy: act on supported; treat unsupported/insufficient_evidence as unverified; reject contradicted. x402 or SANITY_API_KEY; 429 means a rate limit — see retry_after.', { claim: z.string().describe("The single statement to verify (max 5,000 chars)."), context: contextShape, mode: modeShape }, ({ claim, context, mode }) => call("POST", SANITY_URL, "/check", { headers: keyHeader(SANITY_API_KEY), json: { claim, context, mode: mode ?? "grounded" }, timeoutMs: SANITY_TIMEOUT_MS }));
293
+ tool("sanity_check_batch", "[Wicked Sanity] Verify up to 50 claims against one shared context in a single call — prefer this over one sanity_check per sentence. `data` is a list in the same order as `claims`.", { claims: z.array(z.string()).min(1).max(50).describe("1-50 statements, each up to 5,000 chars."), context: contextShape, mode: modeShape }, ({ claims, context, mode }) => call("POST", SANITY_URL, "/check/batch", { headers: keyHeader(SANITY_API_KEY), json: { claims, context, mode: mode ?? "grounded" }, timeoutMs: SANITY_TIMEOUT_MS }));
294
+ // ---- Wicked Memory (wallet-scoped, signature per call) ----
295
+ tool("memory_prepare", 'Wicked Memory step 1 of every call: returns the exact `message_to_sign`. Sign it with your own wallet (EIP-191 personal_sign), then call the matching memory_* tool with the SAME arguments plus the returned timestamp, nonce and your signature. Timestamp must be within 5 minutes; nonce is single-use, so prepare again for every call.', {
296
+ operation: z.string().describe('"store", "search", "get", "update", "history", "delete" or "deletions".'),
297
+ wallet: z.string().describe("Your agent's 0x wallet address (memories are scoped to it)."),
298
+ params: z.record(z.string(), z.unknown()).optional().describe('The arguments you will pass to the tool, by name, e.g. {"content":"...","tags":["a"]} or {"memory_id":"<uuid>"}.'),
299
+ }, async ({ operation, wallet, params }) => {
300
+ let built;
301
+ try {
302
+ built = memoryBuild(operation, params);
303
+ }
304
+ catch (err) {
305
+ return { error: err.message, source: "mcp", http_status: 400 };
306
+ }
307
+ const timestamp = String(Math.floor(Date.now() / 1000));
308
+ const nonce = randomBytes(16).toString("hex");
309
+ return {
310
+ message_to_sign: memoryMessage(wallet, built.method, built.path, timestamp, nonce, built.body),
311
+ timestamp,
312
+ nonce,
313
+ operation,
314
+ wallet,
315
+ request: { method: built.method, path: built.path },
316
+ next: `Sign message_to_sign with the wallet's key (EIP-191 personal_sign), then call memory_${operation} with the same arguments plus timestamp, nonce and signature.`,
317
+ };
318
+ });
319
+ const memWallet = z.string().describe("Your agent's 0x wallet address.");
320
+ const memId = z.string().describe("The memory's UUID.");
321
+ tool("memory_store", "[Wicked Memory] Store a memory (free, fair-use rate limited). A real embedding is computed at write time. Run memory_prepare with operation 'store' and these same arguments first.", {
322
+ wallet: memWallet,
323
+ content: z.string().describe("Text to remember (max 16 KB)."),
324
+ ...sigShape,
325
+ tags: z.array(z.string()).optional().describe("Up to 20 short tags."),
326
+ metadata: z.record(z.string(), z.unknown()).optional().describe("Free-form JSON object (max 8 KB)."),
327
+ source: z.string().optional().describe("Which agent/session wrote this."),
328
+ }, (a) => memoryCall("store", a.wallet, { content: a.content, tags: a.tags, metadata: a.metadata, source: a.source }, a));
329
+ tool("memory_search", "[Wicked Memory] Semantic search over YOUR memories only, ranked by real cosine similarity. The one metered call: free with MEMORY_API_KEY, otherwise http_status 402 with x402 v2 instructions under `payment_required` (0.001 USDC on Base) — pay, then call again with the SAME arguments (a 402 does not consume the nonce) plus payment_signature.", {
330
+ wallet: memWallet,
331
+ q: z.string().describe("What to look for, in natural language."),
332
+ ...sigShape,
333
+ limit: z.coerce.number().optional().describe("1-50, default 10."),
334
+ tags: z.array(z.string()).optional().describe("Only memories having ALL of these tags."),
335
+ created_after: z.string().optional().describe("ISO-8601 lower bound."),
336
+ created_before: z.string().optional().describe("ISO-8601 upper bound."),
337
+ as_of: z.string().optional().describe("ISO-8601 instant; search memory as it stood then."),
338
+ include_superseded: z.coerce.boolean().optional().describe("Also search old, superseded versions."),
339
+ payment_signature: z.string().optional().describe("x402 payment payload (base64) for the keyless path."),
340
+ }, (a) => memoryCall("search", a.wallet, { q: a.q, limit: a.limit, tags: a.tags, created_after: a.created_after, created_before: a.created_before, as_of: a.as_of, include_superseded: a.include_superseded }, a, a.payment_signature));
341
+ tool("memory_get", "[Wicked Memory] Fetch one of your memories by id (another wallet's or a deleted id returns the same 404).", { wallet: memWallet, memory_id: memId, ...sigShape }, (a) => memoryCall("get", a.wallet, { memory_id: a.memory_id }, a));
342
+ tool("memory_update", "[Wicked Memory] Update without overwriting: creates a new version and closes the old one (valid_until / superseded_by). Omitted fields carry over; only the current version can be updated (409 otherwise).", {
343
+ wallet: memWallet,
344
+ memory_id: z.string().describe("UUID of the CURRENT version to supersede."),
345
+ ...sigShape,
346
+ content: z.string().optional().describe("New text (re-embedded). Provide at least one of content/tags/metadata/source."),
347
+ tags: z.array(z.string()).optional().describe("Replacement tags."),
348
+ metadata: z.record(z.string(), z.unknown()).optional().describe("Replacement metadata object."),
349
+ source: z.string().optional().describe("New source label."),
350
+ }, (a) => memoryCall("update", a.wallet, { memory_id: a.memory_id, content: a.content, tags: a.tags, metadata: a.metadata, source: a.source }, a));
351
+ tool("memory_history", "[Wicked Memory] Full version chain for a memory, oldest first, with valid_from / valid_until / superseded_by. Works from any version's id.", { wallet: memWallet, memory_id: z.string().describe("UUID of any version in the chain."), ...sigShape }, (a) => memoryCall("history", a.wallet, { memory_id: a.memory_id }, a));
352
+ tool("memory_delete", '[Wicked Memory] PERMANENTLY hard-delete a memory (right-to-be-forgotten). Default scope "chain" removes every version; "version" only this one. Only an audit row (no content) is kept.', {
353
+ wallet: memWallet,
354
+ memory_id: z.string().describe("UUID of any version in the chain."),
355
+ ...sigShape,
356
+ scope: z.string().optional().describe('"chain" (all versions, default) or "version".'),
357
+ reason: z.string().optional().describe("Optional audit-log note (max 200 chars)."),
358
+ }, (a) => memoryCall("delete", a.wallet, { memory_id: a.memory_id, scope: a.scope ?? "chain", reason: a.reason }, a));
359
+ tool("memory_deletions", "[Wicked Memory] Your deletion audit log (ids, times, reasons; never content).", { wallet: memWallet, ...sigShape }, (a) => memoryCall("deletions", a.wallet, {}, a));
360
+ // ---- x402 Paywall (tenant API; needs PAYWALL_API_KEY except signup) ----
361
+ const routeShape = {
362
+ price: z.string().describe('Decimal USD amount, e.g. "0.01" or "$0.01".'),
363
+ network: z.string().describe("CAIP-2 id: Base mainnet eip155:8453, Base Sepolia eip155:84532."),
364
+ payTo: z.string().describe("Your payout wallet (0x…). Settlement pays here directly; nothing is custodied."),
365
+ method: z.enum(["GET", "POST"]).optional().describe("Default GET."),
366
+ path: z.string().optional().describe('Route path, e.g. "/weather".'),
367
+ asset: z.string().optional().describe("Payment token contract; defaults to USDC on the network."),
368
+ description: z.string().optional().describe("Up to 200 chars: letters, digits, space . _ -"),
369
+ upstreamUrl: z.string().optional().describe("Your https backend. Omit for the demo confirmation payload."),
370
+ };
371
+ tool("paywall_signup", "[x402 Paywall] Create a tenant account. Returns a Bearer apiKey ONCE — store it and set PAYWALL_API_KEY. Rate-limited to 5/hour per IP.", {
372
+ name: z.string().describe("Letters, digits, space . _ - (max 100)."),
373
+ slug: z.string().describe("Lowercase a-z0-9 and hyphens, 2-40 chars; becomes your mount path prefix /{slug}."),
374
+ }, ({ name, slug }) => call("POST", PAYWALL_URL, "/signup", { json: { name, slug } }));
375
+ tool("paywall_get_tenant", "[x402 Paywall] Your tenant record (needs PAYWALL_API_KEY).", {}, () => call("GET", PAYWALL_URL, "/tenants/me", { headers: bearer(PAYWALL_API_KEY) }));
376
+ tool("paywall_list_routes", "[x402 Paywall] List your paywalled routes (needs PAYWALL_API_KEY).", {}, () => call("GET", PAYWALL_URL, "/tenants/me/routes", { headers: bearer(PAYWALL_API_KEY) }));
377
+ tool("paywall_create_route", "[x402 Paywall] Paywall a new endpoint with x402 pay-per-call, settled in USDC straight to payTo. With upstreamUrl, paid requests are proxied to your backend. Applies live within ~20s (needs PAYWALL_API_KEY).", routeShape, (a) => call("POST", PAYWALL_URL, "/tenants/me/routes", { headers: bearer(PAYWALL_API_KEY), json: a }));
378
+ tool("paywall_import_routes", "[x402 Paywall] Bulk upsert routes keyed on (method, path) — paywall a whole API in one call (needs PAYWALL_API_KEY).", { routes: z.array(z.object(routeShape)).min(1).describe("Route objects, same fields as paywall_create_route.") }, ({ routes }) => call("POST", PAYWALL_URL, "/tenants/me/routes/import", { headers: bearer(PAYWALL_API_KEY), json: { routes } }));
379
+ tool("paywall_update_route", "[x402 Paywall] Partial update of a route (e.g. change price or payout wallet); applies live within ~20s (needs PAYWALL_API_KEY).", {
380
+ id: z.string().describe("Route id from paywall_list_routes."),
381
+ price: routeShape.price.optional(),
382
+ network: routeShape.network.optional(),
383
+ payTo: routeShape.payTo.optional(),
384
+ method: routeShape.method,
385
+ path: routeShape.path,
386
+ asset: routeShape.asset,
387
+ description: routeShape.description,
388
+ upstreamUrl: routeShape.upstreamUrl,
389
+ }, ({ id, ...patch }) => call("PATCH", PAYWALL_URL, `/tenants/me/routes/${encodeURIComponent(id)}`, { headers: bearer(PAYWALL_API_KEY), json: patch }));
390
+ tool("paywall_delete_route", "[x402 Paywall] Remove a paywalled route; applies within ~20s (needs PAYWALL_API_KEY).", { id: z.string().describe("Route id from paywall_list_routes.") }, ({ id }) => call("DELETE", PAYWALL_URL, `/tenants/me/routes/${encodeURIComponent(id)}`, { headers: bearer(PAYWALL_API_KEY) }));
391
+ tool("paywall_list_settlements", "[x402 Paywall] Your real on-chain USDC settlements (earnings) (needs PAYWALL_API_KEY).", { limit: z.coerce.number().optional().describe("Default 50, max 200.") }, ({ limit }) => call("GET", PAYWALL_URL, "/tenants/me/settlements", { headers: bearer(PAYWALL_API_KEY), query: { limit } }));
392
+ return n;
393
+ }
394
+ // Exposed for tests: deterministic builders used by memory_prepare.
395
+ export const __test = { memoryBuild, memoryMessage };
package/package.json CHANGED
@@ -1,47 +1,55 @@
1
- {
2
- "name": "wickedapi-mcp",
3
- "version": "0.1.0",
4
- "description": "MCP server for WickedAPI — agent-ready trading & market-data API (crypto/stock prices, indicators, prediction markets, DeFi, macro, AI momentum score). No signup required: pay per call in USDC via x402, or use a partner API key.",
5
- "type": "module",
6
- "bin": {
7
- "wickedapi-mcp": "dist/index.js"
8
- },
9
- "main": "dist/index.js",
10
- "files": [
11
- "dist"
12
- ],
13
- "engines": {
14
- "node": ">=20"
15
- },
16
- "license": "MIT",
17
- "homepage": "https://api.wickedapi.com",
18
- "bugs": {
19
- "url": "https://api.wickedapi.com"
20
- },
21
- "author": "WickedAPI",
22
- "scripts": {
23
- "build": "tsc -p tsconfig.json",
24
- "start": "node dist/index.js",
25
- "prepublishOnly": "npm run build"
26
- },
27
- "keywords": [
28
- "mcp",
29
- "model-context-protocol",
30
- "trading",
31
- "crypto",
32
- "stocks",
33
- "market-data",
34
- "x402",
35
- "agent"
36
- ],
37
- "dependencies": {
38
- "@modelcontextprotocol/sdk": "^1.29.0",
39
- "viem": "^2.55.2",
40
- "x402-fetch": "^1.2.0",
41
- "zod": "^4.4.3"
42
- },
43
- "devDependencies": {
44
- "@types/node": "^22.20.1",
45
- "typescript": "^5.9.3"
46
- }
47
- }
1
+ {
2
+ "name": "wickedapi-mcp",
3
+ "version": "0.3.0",
4
+ "description": "MCP server for the whole Wicked suite — trading data, DeFi protocol health, token safety, brokerage sync, agent identity, reputation, tool registry, hallucination checks, persistent memory, and x402 paywalls. 70 tools, 6 resources, 3 prompts. No signup required: pay per call in USDC via x402, or use a partner API key.",
5
+ "type": "module",
6
+ "bin": {
7
+ "wickedapi-mcp": "dist/index.js"
8
+ },
9
+ "main": "dist/index.js",
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "engines": {
14
+ "node": ">=20"
15
+ },
16
+ "license": "MIT",
17
+ "homepage": "https://api.wickedapi.com",
18
+ "bugs": {
19
+ "url": "https://api.wickedapi.com"
20
+ },
21
+ "author": "WickedAPI",
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.json",
24
+ "start": "node dist/index.js",
25
+ "prepublishOnly": "npm run build"
26
+ },
27
+ "keywords": [
28
+ "mcp",
29
+ "model-context-protocol",
30
+ "trading",
31
+ "crypto",
32
+ "stocks",
33
+ "market-data",
34
+ "defi",
35
+ "brokerage",
36
+ "x402",
37
+ "agent",
38
+ "identity",
39
+ "reputation",
40
+ "registry",
41
+ "memory",
42
+ "sanity",
43
+ "paywall"
44
+ ],
45
+ "dependencies": {
46
+ "@modelcontextprotocol/sdk": "^1.29.0",
47
+ "viem": "^2.55.2",
48
+ "x402-fetch": "^1.2.0",
49
+ "zod": "^4.4.3"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^22.20.1",
53
+ "typescript": "^5.9.3"
54
+ }
55
+ }