wickedapi-mcp 0.3.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,114 +1,11 @@
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
- ## Remote endpoint (no install)
25
-
26
- The same 70 tools are served over streamable HTTP at **`https://mcp.wickedapi.com/mcp`** (stateless, rate-limited per IP; the public deployment carries no personal keys, so paid calls return the x402 challenge). Point any streamable-HTTP MCP client at it. To self-host, build the `Dockerfile` (or `npm run build && npm run start:http`); see `.env.example` for `RATE_LIMIT_PER_MINUTE`, `ALLOWED_HOSTS` and `TRUSTED_CLIENT_IP_HEADER`.
27
-
28
- ## No signup required
29
-
30
- 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:
31
-
32
- - **Zero config** — calls without credentials still work; paid endpoints return the real `402` payment challenge (price, network, `payTo`) as the tool result.
33
- - **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.
34
- - **Partner API key** — set `WICKEDAPI_API_KEY` for free, rate-limited access across all four services (same key works everywhere).
35
-
36
- ## Install
37
-
38
- ```json
39
- {
40
- "mcpServers": {
41
- "wickedapi": {
42
- "command": "npx",
43
- "args": ["-y", "wickedapi-mcp"],
44
- "env": {
45
- "WICKEDAPI_API_KEY": "your-key-here"
46
- }
47
- }
48
- }
49
- }
50
- ```
51
-
52
- Or for autonomous pay-per-call instead of a key:
53
-
54
- ```json
55
- {
56
- "mcpServers": {
57
- "wickedapi": {
58
- "command": "npx",
59
- "args": ["-y", "wickedapi-mcp"],
60
- "env": {
61
- "WICKEDAPI_PAYER_PRIVATE_KEY": "0xyour-base-wallet-private-key"
62
- }
63
- }
64
- }
65
- }
66
- ```
67
-
68
- 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.
69
-
70
- ## Resources
71
-
72
- Read without calling a tool — useful for a client that surfaces resources in its UI:
73
-
74
- - `wickedapi://platform/services` — the full service directory (JSON)
75
- - `wickedapi://platform/llms` — the platform's `llms.txt`
76
- - `wickedapi://trading/status`, `wickedapi://protocol_health/status`, `wickedapi://token_risk/status`, `wickedapi://broker_sync/status` — each service's live endpoint catalog
77
-
78
- ## Prompts
79
-
80
- One-click workflows that chain real tool calls (Claude Desktop and other prompt-aware clients surface these directly):
81
-
82
- - **`rug_check`** (`address`, `chain`, optional `symbol`) — token risk check + market sentiment + optional momentum read, before buying.
83
- - **`vet_dependency`** (`slug`, optional `token_address`, `chain`) — protocol health + optional contract-level risk check, before building on a DeFi protocol.
84
- - **`morning_briefing`** (optional `symbol`) — macro calendar + earnings calendar + sentiment + momentum verdict, as one digest.
85
-
86
- ## Environment variables
87
-
88
- | Var | Required | Notes |
89
- | --- | --- | --- |
90
- | `WICKEDAPI_API_KEY` | no | Free, rate-limited access across all four services (same key works everywhere). |
91
- | `WICKEDAPI_PAYER_PRIVATE_KEY` | no | Base-mainnet wallet private key. If set, every call auto-pays the x402 challenge in USDC — no key needed. |
92
- | `WICKEDAPI_BASE_URL` | no | Override the Trading Data API base (default `https://api.wickedapi.com`). |
93
- | `WICKEDAPI_PROTOCOL_HEALTH_URL` | no | Override the Protocol Health Oracle base (default `https://protocol-health-oracle.wickedapi.com`). |
94
- | `WICKEDAPI_TOKEN_RISK_URL` | no | Override the Token Risk Oracle base (default `https://token-risk-oracle.wickedapi.com`). |
95
- | `WICKEDAPI_BROKER_SYNC_URL` | no | Override the Broker Sync API base (default `https://broker-sync-api.wickedapi.com`). |
96
- | `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. |
97
- | `PAYWALL_API_KEY` | for `paywall_*` | Tenant Bearer key returned once by `paywall_signup`. |
98
- | `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). |
99
- | `WICKEDAPI_SHOWCASE_URL` | no | Override the platform directory base used by resources (default `https://wickedapi.com`). |
100
-
101
- 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.
102
-
103
- ## Local development
104
-
105
- ```bash
106
- npm install
107
- npm test # builds, then unit + end-to-end tests (e2e needs network)
108
- node dist/index.js # stdio
109
- node dist/http.js # streamable HTTP on $PORT (default 8080)
110
- ```
111
-
112
- ## What this is not
113
-
114
- 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.
1
+ # wickedapi-mcp (moved)
2
+
3
+ This package is now **[@wickedlabs/wicked-mcp](https://www.npmjs.com/package/@wickedlabs/wicked-mcp)**.
4
+
5
+ This release only forwards to it, so existing setups keep working. Please update your MCP client config:
6
+
7
+ ```json
8
+ { "command": "npx", "args": ["-y", "@wickedlabs/wicked-mcp"] }
9
+ ```
10
+
11
+ Hosted (no install): `https://mcp.wickedapi.com/mcp` · Source: https://github.com/choaticpixels/wicked-mcp
package/bin.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ // `wickedapi-mcp` moved to `@wickedlabs/wicked-mcp`. This forwards to it so
3
+ // existing `npx -y wickedapi-mcp` configs keep working. stderr only: stdout is
4
+ // the MCP protocol stream.
5
+ console.error("[wickedapi-mcp] This package has moved to @wickedlabs/wicked-mcp. Update your config to `npx -y @wickedlabs/wicked-mcp`.");
6
+ await import("@wickedlabs/wicked-mcp/dist/index.js");
package/package.json CHANGED
@@ -1,62 +1,29 @@
1
- {
2
- "name": "wickedapi-mcp",
3
- "version": "0.3.1",
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://mcp.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
- "start:http": "node dist/http.js",
27
- "test": "npm run build && node --test \"test/*.test.mjs\""
28
- },
29
- "keywords": [
30
- "mcp",
31
- "model-context-protocol",
32
- "trading",
33
- "crypto",
34
- "stocks",
35
- "market-data",
36
- "defi",
37
- "brokerage",
38
- "x402",
39
- "agent",
40
- "identity",
41
- "reputation",
42
- "registry",
43
- "memory",
44
- "sanity",
45
- "paywall"
46
- ],
47
- "dependencies": {
48
- "@modelcontextprotocol/sdk": "^1.29.0",
49
- "viem": "^2.55.2",
50
- "x402-fetch": "^1.2.0",
51
- "zod": "^4.4.3"
52
- },
53
- "devDependencies": {
54
- "@types/node": "^22.20.1",
55
- "typescript": "^5.9.3"
56
- },
57
- "repository": {
58
- "type": "git",
59
- "url": "git+https://github.com/choaticpixels/wicked-mcp.git"
60
- },
61
- "mcpName": "io.github.choaticpixels/wicked-mcp"
62
- }
1
+ {
2
+ "name": "wickedapi-mcp",
3
+ "version": "0.5.1",
4
+ "description": "MOVED: this package is now @wickedlabs/wicked-mcp. This release forwards to it, so existing `npx wickedapi-mcp` setups keep working.",
5
+ "type": "module",
6
+ "bin": {
7
+ "wickedapi-mcp": "bin.js"
8
+ },
9
+ "files": [
10
+ "bin.js",
11
+ "README.md"
12
+ ],
13
+ "dependencies": {
14
+ "@wickedlabs/wicked-mcp": "^0.5.0"
15
+ },
16
+ "engines": {
17
+ "node": ">=20"
18
+ },
19
+ "license": "MIT",
20
+ "author": "WickedLabs",
21
+ "homepage": "https://mcp.wickedapi.com",
22
+ "mcpName": "io.github.choaticpixels/wicked-mcp",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/choaticpixels/wicked-mcp.git",
26
+ "directory": "shim"
27
+ },
28
+ "keywords": ["mcp", "model-context-protocol", "wicked", "x402"]
29
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 choaticpixels
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
package/dist/clientIp.js DELETED
@@ -1,67 +0,0 @@
1
- // Who is calling, for rate limiting. Ported from the Python http_app.py so
2
- // behaviour is unchanged after the cutover.
3
- //
4
- // Behind Railway's edge proxy the TCP peer is always an internal address, so
5
- // the limiter keys on the proxy-set X-Real-IP header instead. That is only
6
- // safe because the proxy sets the header itself; set TRUSTED_CLIENT_IP_HEADER
7
- // to empty to fall back to the TCP peer when running without a proxy, or to
8
- // cf-connecting-ip if the domain is ever proxied through Cloudflare.
9
- import { isIP } from "node:net";
10
- const TRUSTED_HEADER = (process.env.TRUSTED_CLIENT_IP_HEADER ?? "x-real-ip").trim().toLowerCase();
11
- function expandIPv6(ip) {
12
- let s = ip.split("%")[0];
13
- let tail = [];
14
- const v4 = s.match(/(\d+\.\d+\.\d+\.\d+)$/);
15
- if (v4) {
16
- const o = v4[1].split(".").map(Number);
17
- tail = [(o[0] << 8) | o[1], (o[2] << 8) | o[3]];
18
- s = s.slice(0, -v4[1].length) + "0:0";
19
- }
20
- const halves = s.split("::");
21
- if (halves.length > 2)
22
- return null;
23
- const head = halves[0] ? halves[0].split(":") : [];
24
- const rest = halves.length === 2 && halves[1] ? halves[1].split(":") : [];
25
- const fill = 8 - head.length - rest.length;
26
- if ((halves.length === 1 && fill !== 0) || fill < 0)
27
- return null;
28
- const groups = [...head, ...Array(halves.length === 2 ? fill : 0).fill("0"), ...rest].map((g) => parseInt(g, 16));
29
- if (groups.length !== 8 || groups.some((g) => Number.isNaN(g)))
30
- return null;
31
- if (tail.length) {
32
- groups[6] = tail[0];
33
- groups[7] = tail[1];
34
- }
35
- return groups;
36
- }
37
- /** Canonical rate-limit key, or null if `raw` isn't an IP. IPv4-mapped IPv6
38
- * collapses to the IPv4 address; IPv6 is grouped by /64 so rotating addresses
39
- * inside one prefix can't dodge the limit. */
40
- export function normalizeIp(raw) {
41
- const s = raw.trim();
42
- const kind = isIP(s);
43
- if (kind === 4)
44
- return s;
45
- if (kind !== 6)
46
- return null;
47
- const g = expandIPv6(s);
48
- if (!g)
49
- return null;
50
- if (g.slice(0, 5).every((x) => x === 0) && g[5] === 0xffff) {
51
- return `${g[6] >> 8}.${g[6] & 255}.${g[7] >> 8}.${g[7] & 255}`;
52
- }
53
- return `${g.slice(0, 4).map((x) => x.toString(16)).join(":")}::/64`;
54
- }
55
- export function clientKey(req) {
56
- if (TRUSTED_HEADER) {
57
- const raw = req.headers[TRUSTED_HEADER];
58
- const value = Array.isArray(raw) ? raw[0] : raw;
59
- if (value) {
60
- const key = normalizeIp(value);
61
- if (key)
62
- return key;
63
- }
64
- }
65
- const peer = req.socket.remoteAddress ?? "unknown";
66
- return normalizeIp(peer) ?? peer;
67
- }
package/dist/http.js DELETED
@@ -1,82 +0,0 @@
1
- // Streamable-HTTP entry point for the public remote at https://mcp.wickedapi.com/mcp.
2
- // Stateless: every request gets its own McpServer + transport (specs are
3
- // loaded once at boot), so it scales and restarts without session loss.
4
- // Public-endpoint additions over the stdio build: a per-IP rate limit and a
5
- // Host allowlist (DNS-rebinding protection). Tools and upstream calls are
6
- // identical.
7
- import { createServer } from "node:http";
8
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
9
- import { clientKey } from "./clientIp.js";
10
- import { rateLimited } from "./rateLimit.js";
11
- import { buildServer, loadWorld, SERVER_VERSION } from "./server.js";
12
- const PORT = Number(process.env.PORT || 8080);
13
- const RATE_LIMIT_PER_MINUTE = Number(process.env.RATE_LIMIT_PER_MINUTE || 30);
14
- const MAX_BODY_BYTES = 1_000_000;
15
- const ALLOWED_HOSTS = (process.env.ALLOWED_HOSTS || "mcp.wickedapi.com,wicked-mcp-production.up.railway.app,localhost:8080,127.0.0.1:8080")
16
- .split(",")
17
- .map((h) => h.trim().toLowerCase())
18
- .filter(Boolean);
19
- function send(res, status, body, headers = {}) {
20
- res.writeHead(status, { "content-type": "application/json", ...headers });
21
- res.end(JSON.stringify(body));
22
- }
23
- async function readJson(req) {
24
- const chunks = [];
25
- let size = 0;
26
- for await (const chunk of req) {
27
- size += chunk.length;
28
- if (size > MAX_BODY_BYTES)
29
- throw Object.assign(new Error("payload too large"), { status: 413 });
30
- chunks.push(chunk);
31
- }
32
- if (!chunks.length)
33
- return undefined;
34
- try {
35
- return JSON.parse(Buffer.concat(chunks).toString("utf8"));
36
- }
37
- catch {
38
- throw Object.assign(new Error("invalid JSON"), { status: 400 });
39
- }
40
- }
41
- async function main() {
42
- const world = await loadWorld();
43
- const { summary } = buildServer(world);
44
- const http = createServer(async (req, res) => {
45
- const url = new URL(req.url || "/", "http://x");
46
- if (url.pathname === "/health")
47
- return send(res, 200, { status: "ok", version: SERVER_VERSION });
48
- const host = (req.headers.host || "").toLowerCase();
49
- if (!ALLOWED_HOSTS.includes(host))
50
- return send(res, 421, { error: "Invalid Host header" });
51
- if (url.pathname !== "/mcp")
52
- return send(res, 404, { error: "Not found. The MCP endpoint is /mcp." });
53
- if (req.method !== "POST") {
54
- return send(res, 405, { jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed (stateless server: POST only)." }, id: null }, { allow: "POST" });
55
- }
56
- if (rateLimited(clientKey(req), RATE_LIMIT_PER_MINUTE)) {
57
- return send(res, 429, { error: `Rate limit exceeded: ${RATE_LIMIT_PER_MINUTE} requests/minute per IP. Run this server locally (npx wickedapi-mcp) for unlimited use.` }, { "retry-after": "60" });
58
- }
59
- try {
60
- const body = await readJson(req);
61
- const { server } = buildServer(world);
62
- const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
63
- res.on("close", () => {
64
- void transport.close();
65
- void server.close();
66
- });
67
- await server.connect(transport);
68
- await transport.handleRequest(req, res, body);
69
- }
70
- catch (err) {
71
- const status = err.status ?? 500;
72
- if (!res.headersSent) {
73
- send(res, status, { jsonrpc: "2.0", error: { code: -32603, message: err.message }, id: null });
74
- }
75
- }
76
- });
77
- http.listen(PORT, "0.0.0.0", () => console.error(`[wickedapi-mcp] http on :${PORT}/mcp - ${summary}.`));
78
- }
79
- main().catch((err) => {
80
- console.error("[wickedapi-mcp] fatal:", err);
81
- process.exit(1);
82
- });
package/dist/index.js DELETED
@@ -1,13 +0,0 @@
1
- #!/usr/bin/env node
2
- // stdio entry point: `npx wickedapi-mcp` for Claude Desktop, Cursor, .mcpb, etc.
3
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { buildServer, loadWorld } from "./server.js";
5
- async function main() {
6
- const { server, summary } = buildServer(await loadWorld());
7
- await server.connect(new StdioServerTransport());
8
- console.error(`[wickedapi-mcp] ready - ${summary}.`);
9
- }
10
- main().catch((err) => {
11
- console.error("[wickedapi-mcp] fatal:", err);
12
- process.exit(1);
13
- });
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,332 +0,0 @@
1
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { z } from "zod";
3
- import { registerSuiteTools } from "./suite.js";
4
- function envUrl(name, fallback) {
5
- return (process.env[name] || fallback).replace(/\/$/, "");
6
- }
7
- const SERVICES = [
8
- { id: "trading", name: "Trading Data API", baseUrl: envUrl("WICKEDAPI_BASE_URL", "https://api.wickedapi.com") },
9
- { id: "protocol_health", name: "Protocol Health Oracle", baseUrl: envUrl("WICKEDAPI_PROTOCOL_HEALTH_URL", "https://protocol-health-oracle.wickedapi.com") },
10
- { id: "token_risk", name: "Token Risk Oracle", baseUrl: envUrl("WICKEDAPI_TOKEN_RISK_URL", "https://token-risk-oracle.wickedapi.com") },
11
- { id: "broker_sync", name: "Broker Sync API", baseUrl: envUrl("WICKEDAPI_BROKER_SYNC_URL", "https://broker-sync-api.wickedapi.com") },
12
- ];
13
- const SHOWCASE_URL = envUrl("WICKEDAPI_SHOWCASE_URL", "https://wickedapi.com");
14
- const API_KEY = process.env.WICKEDAPI_API_KEY;
15
- const PAYER_KEY = process.env.WICKEDAPI_PAYER_PRIVATE_KEY;
16
- // A few auto-generated tool names collide or read badly (e.g. GET and POST
17
- // both hitting /v1/protocols) — override just those, keyed by
18
- // "METHOD /openapi/path/{param}".
19
- const TOOL_NAME_OVERRIDES = {
20
- protocol_health: {
21
- "GET /v1/protocols": "protocol_health_list_protocols",
22
- "POST /v1/protocols": "protocol_health_track_protocol",
23
- "GET /v1/health/{slug}": "protocol_health_score",
24
- "POST /v1/refresh": "protocol_health_refresh_all",
25
- "POST /v1/ingest": "protocol_health_sync_universe",
26
- },
27
- broker_sync: {
28
- "POST /v1/users": "broker_sync_register_user",
29
- },
30
- };
31
- // If a funded Base-mainnet wallet key is provided, every call auto-pays the
32
- // x402 402 challenge in USDC — no API key or signup needed at all. Loaded
33
- // lazily so a plain API-key setup never has to pull in viem/x402-fetch.
34
- async function buildFetcher() {
35
- if (!PAYER_KEY)
36
- return fetch;
37
- let deps;
38
- try {
39
- deps = await Promise.all([import("viem"), import("viem/accounts"), import("viem/chains"), import("x402-fetch")]);
40
- }
41
- catch {
42
- console.error("[wickedapi-mcp] WICKEDAPI_PAYER_PRIVATE_KEY is set, but the wallet-payment dependencies " +
43
- "(viem, x402-fetch) aren't installed in this distribution. Falling back to plain fetch — " +
44
- "calls without an API key will surface the 402 challenge instead of auto-paying. " +
45
- "Use `npx wickedapi-mcp` for full auto-pay support.");
46
- return fetch;
47
- }
48
- const [{ createWalletClient, http }, { privateKeyToAccount }, { base }, { wrapFetchWithPayment }] = deps;
49
- const account = privateKeyToAccount(PAYER_KEY);
50
- const wallet = createWalletClient({ account, chain: base, transport: http() });
51
- console.error(`[wickedapi-mcp] x402 auto-pay enabled — wallet ${account.address}`);
52
- // x402-fetch's Signer type expects a client extended with viem's public
53
- // actions; the proven pattern in scripts/x402-bot-test.mjs passes a plain
54
- // wallet client the same way — cast rather than pull in publicActions.
55
- return wrapFetchWithPayment(fetch, wallet);
56
- }
57
- function pathSlug(openapiPath) {
58
- return openapiPath
59
- .replace(/^\/v1\//, "")
60
- .replace(/[{}]/g, "")
61
- .replace(/[\/\-]+/g, "_");
62
- }
63
- function toolNameFor(service, method, openapiPath) {
64
- const override = TOOL_NAME_OVERRIDES[service.id]?.[`${method.toUpperCase()} ${openapiPath}`];
65
- if (override)
66
- return override;
67
- const slug = pathSlug(openapiPath);
68
- if (service.id === "trading")
69
- return slug; // legacy naming, already published
70
- return slug === service.id ? service.id : `${service.id}_${slug}`;
71
- }
72
- function zodFor(p) {
73
- const base = p.type === "number" || p.type === "integer"
74
- ? z.coerce.number()
75
- : p.type === "boolean"
76
- ? z.coerce.boolean()
77
- : z.string();
78
- const described = p.description ? base.describe(p.description) : base;
79
- return p.required ? described : described.optional();
80
- }
81
- async function loadServiceSpec(service) {
82
- try {
83
- const res = await fetch(`${service.baseUrl}/openapi.json`);
84
- if (!res.ok)
85
- throw new Error(`HTTP ${res.status}`);
86
- return (await res.json());
87
- }
88
- catch (err) {
89
- console.error(`[wickedapi-mcp] WARNING: couldn't load ${service.name} spec (${service.baseUrl}/openapi.json): ` +
90
- `${err.message} — its tools are unavailable this session, the rest load normally.`);
91
- return null;
92
- }
93
- }
94
- function registerServiceTools(server, service, doc, doFetch) {
95
- let registered = 0;
96
- for (const [openapiPath, methods] of Object.entries(doc.paths)) {
97
- if (!openapiPath.startsWith("/v1/") || openapiPath === "/v1/status")
98
- continue; // /v1/status is a resource instead
99
- if (openapiPath.includes("webhook"))
100
- continue; // called by a third party, never by an agent
101
- for (const [method, op] of Object.entries(methods)) {
102
- const pathParamNames = [];
103
- const queryParamNames = [];
104
- const bodyPropNames = [];
105
- const shape = {};
106
- for (const p of op.parameters ?? []) {
107
- if (p.in === "path") {
108
- pathParamNames.push(p.name);
109
- shape[p.name] = zodFor({ type: p.schema?.type, required: true, description: p.description });
110
- }
111
- else if (p.in === "query") {
112
- queryParamNames.push(p.name);
113
- shape[p.name] = zodFor({ type: p.schema?.type, required: p.required, description: p.description });
114
- }
115
- }
116
- const bodySchema = op.requestBody?.content?.["application/json"]?.schema;
117
- if (bodySchema) {
118
- const required = new Set(bodySchema.required ?? []);
119
- for (const [name, prop] of Object.entries(bodySchema.properties ?? {})) {
120
- bodyPropNames.push(name);
121
- shape[name] = zodFor({ type: prop.type, required: required.has(name), description: prop.description });
122
- }
123
- }
124
- const name = toolNameFor(service, method, openapiPath);
125
- server.registerTool(name, {
126
- description: `[${service.name}] ${[op.summary, op.description].filter(Boolean).join(" — ")}`.slice(0, 1000),
127
- inputSchema: shape,
128
- }, async (args) => {
129
- let urlPath = openapiPath;
130
- for (const p of pathParamNames)
131
- urlPath = urlPath.replace(`{${p}}`, encodeURIComponent(String(args[p])));
132
- const url = new URL(service.baseUrl + urlPath);
133
- for (const p of queryParamNames) {
134
- const v = args[p];
135
- if (v !== undefined && v !== null)
136
- url.searchParams.set(p, String(v));
137
- }
138
- const headers = {};
139
- if (API_KEY)
140
- headers["x-api-key"] = API_KEY;
141
- const init = { method: method.toUpperCase(), headers };
142
- if (bodyPropNames.length) {
143
- const bodyObj = {};
144
- for (const p of bodyPropNames)
145
- if (args[p] !== undefined)
146
- bodyObj[p] = args[p];
147
- headers["content-type"] = "application/json";
148
- init.body = JSON.stringify(bodyObj);
149
- }
150
- const r = await doFetch(url.toString(), init);
151
- const text = await r.text();
152
- let body;
153
- try {
154
- body = JSON.parse(text);
155
- }
156
- catch {
157
- body = text;
158
- }
159
- return {
160
- isError: !r.ok,
161
- content: [{ type: "text", text: JSON.stringify(body, null, 2) }],
162
- };
163
- });
164
- registered++;
165
- }
166
- }
167
- return registered;
168
- }
169
- // --- Resources -----------------------------------------------------------
170
- // Reference material an agent can read without invoking a tool: the
171
- // platform-wide directory/llms.txt, and each service's live endpoint
172
- // catalog (the /v1/status we deliberately excluded from becoming a tool).
173
- function registerResources(server) {
174
- server.registerResource("platform-services", "wickedapi://platform/services", {
175
- title: "WickedAPI service directory",
176
- description: "Every live WickedAPI service — URL, category, one-line description, auth modes, and links to each one's own OpenAPI spec/docs/llms.txt.",
177
- mimeType: "application/json",
178
- }, async (uri) => {
179
- const r = await fetch(`${SHOWCASE_URL}/services.json`);
180
- if (!r.ok)
181
- throw new Error(`Failed to load service directory: HTTP ${r.status}`);
182
- return { contents: [{ uri: uri.toString(), mimeType: "application/json", text: await r.text() }] };
183
- });
184
- server.registerResource("platform-llms-txt", "wickedapi://platform/llms", {
185
- title: "WickedAPI llms.txt",
186
- description: "Terse, platform-wide summary for agents deciding what to call — auth modes, free vs. paid endpoints.",
187
- mimeType: "text/plain",
188
- }, async (uri) => {
189
- const r = await fetch(`${SHOWCASE_URL}/llms.txt`);
190
- if (!r.ok)
191
- throw new Error(`Failed to load llms.txt: HTTP ${r.status}`);
192
- return { contents: [{ uri: uri.toString(), mimeType: "text/plain", text: await r.text() }] };
193
- });
194
- for (const service of SERVICES) {
195
- server.registerResource(`${service.id}-status`, `wickedapi://${service.id}/status`, {
196
- title: `${service.name} — live catalog`,
197
- description: `Live endpoint catalog for ${service.name} (${service.baseUrl}), fetched fresh on every read.`,
198
- mimeType: "application/json",
199
- }, async (uri) => {
200
- const r = await fetch(`${service.baseUrl}/v1/status`);
201
- if (!r.ok)
202
- throw new Error(`Failed to load ${service.name} status: HTTP ${r.status}`);
203
- return { contents: [{ uri: uri.toString(), mimeType: "application/json", text: await r.text() }] };
204
- });
205
- }
206
- }
207
- // --- Prompts ---------------------------------------------------------------
208
- // The "compound recipes" from the WickedAPI cookbook, turned into one-click
209
- // prompts: each just tells the assistant which real tools to call and how
210
- // to read the results — no fabricated synthesis, every number cited comes
211
- // from an actual tool call made during the conversation.
212
- function registerPrompts(server) {
213
- server.registerPrompt("rug_check", {
214
- title: "Rug-check before buying",
215
- 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.",
216
- argsSchema: {
217
- address: z.string().describe("The token contract address (EVM) or mint address (Solana)."),
218
- chain: z.string().describe("ethereum, bsc, base, polygon, arbitrum, optimism, avalanche, fantom, zksync, linea, scroll, or solana."),
219
- 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."),
220
- },
221
- }, ({ address, chain, symbol }) => ({
222
- description: "Rug-check workflow",
223
- messages: [
224
- {
225
- role: "user",
226
- content: {
227
- type: "text",
228
- text: `Before I consider buying this token, run a real risk check — don't guess or assume anything:\n\n` +
229
- `1. Call the \`token_risk\` tool with address="${address}" and chain="${chain}". Report the exact ` +
230
- `risk_score, risk_tier, and every real flag it returns (honeypot, mint/ownership authority, holder ` +
231
- `and LP concentration, buy/sell tax, trust-list status) — cite the actual values, don't summarize ` +
232
- `away the specifics or invent ones that weren't returned.\n` +
233
- `2. Call the \`sentiment\` tool for the current overall crypto market mood.\n` +
234
- (symbol
235
- ? `3. Call the \`momentum_score\` tool with symbol="${symbol}" and timeframe="1h" for a real technical read.\n`
236
- : "") +
237
- `\nThen give a plain verdict: the real risk signals found (listed, not paraphrased), current market ` +
238
- `mood, and — only if applicable — the momentum read. State clearly this is aggregated real signals, ` +
239
- `never a safety guarantee.`,
240
- },
241
- },
242
- ],
243
- }));
244
- server.registerPrompt("vet_dependency", {
245
- title: "Vet a DeFi dependency",
246
- description: "Protocol activity/treasury health plus an optional contract-level risk check, before you build on a DeFi protocol.",
247
- argsSchema: {
248
- slug: z.string().describe('The protocol\'s DefiLlama slug, e.g. "lido".'),
249
- token_address: z.string().optional().describe("The protocol's token contract address, if you also want a contract-level risk check."),
250
- chain: z.string().optional().describe("Chain for token_address (default ethereum)."),
251
- },
252
- }, ({ slug, token_address, chain }) => ({
253
- description: "Dependency-vetting workflow",
254
- messages: [
255
- {
256
- role: "user",
257
- content: {
258
- type: "text",
259
- text: `Before I integrate this protocol as a dependency, check its real health — don't guess:\n\n` +
260
- `1. Call the \`protocol_health_score\` tool with slug="${slug}". Report the components (GitHub ` +
261
- `activity, TVL trend, treasury) exactly as returned, including any that came back null (never ` +
262
- `filled in).\n` +
263
- (token_address
264
- ? `2. Call the \`token_risk\` tool with address="${token_address}" and chain="${chain || "ethereum"}" ` +
265
- `for a contract-level check on its token.\n`
266
- : "") +
267
- `\nSummarize: is this protocol actively maintained, is its TVL trend healthy, and (if checked) does ` +
268
- `its token carry any real contract-level risk flags. Cite the real numbers.`,
269
- },
270
- },
271
- ],
272
- }));
273
- server.registerPrompt("morning_briefing", {
274
- title: "Morning briefing",
275
- description: "One daily digest from macro calendar, earnings calendar, sentiment, and a momentum verdict — all free or a fraction of a cent.",
276
- argsSchema: {
277
- symbol: z.string().optional().describe("Asset for the momentum verdict (default BTC). One of BTC, ETH, SOL, SUI, NVDA, IBIT, COIN, QQQ, TQQQ, DIA."),
278
- },
279
- }, ({ symbol }) => {
280
- const sym = symbol || "BTC";
281
- return {
282
- description: "Morning briefing workflow",
283
- messages: [
284
- {
285
- role: "user",
286
- content: {
287
- type: "text",
288
- text: `Compile my morning briefing from real, current data — call each tool, don't estimate:\n\n` +
289
- `1. \`macro_calendar\` with days=7 — upcoming FOMC/CPI/jobs events.\n` +
290
- `2. \`earnings_calendar\` with days=3 — upcoming earnings reports.\n` +
291
- `3. \`sentiment\` — current crypto Fear & Greed reading.\n` +
292
- `4. \`momentum_score\` with symbol="${sym}" and timeframe="1D" — today's technical verdict.\n\n` +
293
- `Then write one concise digest combining all four, citing the real values from each call.`,
294
- },
295
- },
296
- ],
297
- };
298
- });
299
- }
300
- export const SERVER_VERSION = "0.3.1";
301
- export async function loadWorld() {
302
- const doFetch = await buildFetcher();
303
- const specs = [];
304
- for (const service of SERVICES) {
305
- const doc = await loadServiceSpec(service);
306
- if (doc)
307
- specs.push({ service, doc });
308
- }
309
- if (specs.length === 0) {
310
- throw new Error("No service specs loaded - check network access and WICKEDAPI_*_URL overrides.");
311
- }
312
- return { doFetch, specs };
313
- }
314
- export function buildServer(world) {
315
- const server = new McpServer({ name: "wickedapi", version: SERVER_VERSION });
316
- let totalTools = 0;
317
- const loaded = [];
318
- for (const { service, doc } of world.specs) {
319
- totalTools += registerServiceTools(server, service, doc, world.doFetch);
320
- loaded.push(service.name);
321
- }
322
- // Reputation, Registry, Identity, Sanity, Memory and Paywall use hand-written
323
- // tools (non-/v1 paths, wallet-signature auth), registered unconditionally.
324
- totalTools += registerSuiteTools(server);
325
- loaded.push("Reputation, Registry, Identity, Sanity, Memory, Paywall");
326
- registerResources(server);
327
- registerPrompts(server);
328
- return {
329
- server,
330
- summary: `${totalTools} tools across ${loaded.length} group(s) (${loaded.join(", ")}), ${2 + SERVICES.length} resources, 3 prompts`,
331
- };
332
- }
package/dist/suite.js DELETED
@@ -1,395 +0,0 @@
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 };