@wickedlabs/wicked-mcp 0.0.0-stage → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +126 -2
- package/dist/clientIp.js +67 -0
- package/dist/http.js +106 -0
- package/dist/index.js +13 -0
- package/dist/landing.js +96 -0
- package/dist/observability.js +62 -0
- package/dist/prompts.js +85 -0
- package/dist/rateLimit.js +22 -0
- package/dist/server.js +337 -0
- package/dist/suite.js +424 -0
- package/dist/usage.js +46 -0
- package/package.json +61 -4
package/dist/suite.js
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
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 API key (pass api_key, or set REGISTRY_API_KEY). No key yet? Call registry_onboarding_message then registry_create_api_key to get one yourself. For a quick free check of a known tool, use registry_lookup.", {
|
|
232
|
+
api_key: z.string().optional().describe("Free Wicked Registry API key (wr_...). Overrides REGISTRY_API_KEY."),
|
|
233
|
+
category: z.string().optional().describe('e.g. "data", "crypto".'),
|
|
234
|
+
protocol: z.string().optional().describe('"x402", "mcp" or "rest".'),
|
|
235
|
+
min_score: z.coerce.number().optional().describe("Minimum composite score 0-100; unchecked tools are excluded once set."),
|
|
236
|
+
sort: z.string().optional().describe('"score" (default), "latency" or "recency".'),
|
|
237
|
+
limit: z.coerce.number().optional().describe("1-200, default 50."),
|
|
238
|
+
}, ({ api_key, ...a }) => call("GET", REGISTRY_URL, "/tools", { headers: keyHeader(api_key || REGISTRY_API_KEY), query: { sort: "score", limit: 50, ...clean(a) } }));
|
|
239
|
+
tool("registry_tool_detail", "[Wicked Registry] One tool's full score breakdown, component history and recent real health checks. x402 or a free API key.", {
|
|
240
|
+
tool_id: z.string().describe("Registry id from registry_lookup / registry_search_tools / registry_featured_tools."),
|
|
241
|
+
api_key: z.string().optional().describe("Free Wicked Registry API key (wr_...). Overrides REGISTRY_API_KEY."),
|
|
242
|
+
}, ({ tool_id, api_key }) => call("GET", REGISTRY_URL, `/tools/${encodeURIComponent(tool_id)}`, { headers: keyHeader(api_key || REGISTRY_API_KEY) }));
|
|
243
|
+
tool("registry_lookup", "[Wicked Registry] Free lookup: find a tool's id and headline reliability score by name or URL fragment (q), exact endpoint URL (endpoint) or category. No key or payment. Use it to check a tool before your agent pays it; follow with registry_tool_badge or registry_tool_detail.", {
|
|
244
|
+
q: z.string().optional().describe("Name or URL fragment, at least 2 characters."),
|
|
245
|
+
endpoint: z.string().optional().describe("Exact endpoint URL of the tool you are about to call."),
|
|
246
|
+
category: z.string().optional(),
|
|
247
|
+
limit: z.coerce.number().optional().describe("1-25, default 10."),
|
|
248
|
+
}, (a) => call("GET", REGISTRY_URL, "/lookup", { query: clean(a) }));
|
|
249
|
+
tool("registry_onboarding_message", "[Wicked Registry] Step 1 of self-onboarding. Returns the exact message YOU must sign (EIP-191 personal_sign) plus its timestamp, valid for about an hour. action=api_key (needs wallet) for a free API key, or action=register (needs name and endpoint_url) to list your own tool. This tool never signs anything.", {
|
|
250
|
+
action: z.enum(["api_key", "register"]),
|
|
251
|
+
wallet: z.string().optional().describe("Your 0x wallet address (required for api_key)."),
|
|
252
|
+
name: z.string().optional().describe("Tool name (required for register)."),
|
|
253
|
+
endpoint_url: z.string().optional().describe("Tool endpoint URL (required for register)."),
|
|
254
|
+
}, (a) => call("GET", REGISTRY_URL, "/onboard", { query: { action: a.action, wallet: a.wallet, name: a.name, endpointUrl: a.endpoint_url } }));
|
|
255
|
+
tool("registry_create_api_key", "[Wicked Registry] Step 2 of getting a free API key. Submit the wallet, the timestamp and YOUR signature over the message from registry_onboarding_message(action=api_key). The key (wr_...) is returned once - store it and pass it as api_key to registry_search_tools / registry_tool_detail. Up to 3 active keys per wallet; past the daily cap calls fall back to x402.", {
|
|
256
|
+
wallet: z.string().describe("0x wallet that signed the message."),
|
|
257
|
+
timestamp: z.string().describe("The timestamp returned with the message."),
|
|
258
|
+
signature: z.string().describe("0x-prefixed ECDSA signature over the message."),
|
|
259
|
+
label: z.string().optional().describe("Optional label for the key."),
|
|
260
|
+
}, (a) => call("POST", REGISTRY_URL, "/keys", { json: { wallet: a.wallet, timestamp: a.timestamp, signature: a.signature, label: a.label } }));
|
|
261
|
+
tool("registry_featured_tools", "[Wicked Registry] Top scored active tools. Public and free.", {}, () => call("GET", REGISTRY_URL, "/featured-tools"));
|
|
262
|
+
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`));
|
|
263
|
+
tool("registry_report_tool", "[Wicked Registry] File a complaint about a tool's reliability or behaviour. Public, rate-limited; reviewed manually, never auto-suspends.", {
|
|
264
|
+
tool_id: z.string().describe("The tool's Wicked Registry id."),
|
|
265
|
+
reporter: z.string().describe("Your wallet address or another best-effort identifier."),
|
|
266
|
+
reason: z.string().describe('"downtime", "incorrect_response", "scam_or_abuse", "schema_violation" or "other".'),
|
|
267
|
+
evidence: z.string().optional().describe("Optional note or URL."),
|
|
268
|
+
}, ({ tool_id, reporter, reason, evidence }) => call("POST", REGISTRY_URL, `/tools/${encodeURIComponent(tool_id)}/report`, { json: { reporter, reason, evidence } }));
|
|
269
|
+
tool("registry_register_tool", "[Wicked Registry] Register a tool you own so it gets real synthetic checks and a live score. Get the exact message to sign from registry_onboarding_message(action=register), sign it with owner_wallet, then call this. This tool never signs.", {
|
|
270
|
+
name: z.string(),
|
|
271
|
+
endpoint_url: z.string().describe("Live, publicly reachable endpoint."),
|
|
272
|
+
protocol: z.string().describe('"x402", "mcp" or "rest".'),
|
|
273
|
+
category: z.string(),
|
|
274
|
+
description: z.string(),
|
|
275
|
+
owner_wallet: z.string().describe("0x wallet that signed the registration message."),
|
|
276
|
+
signature: z.string().describe("0x-prefixed ECDSA signature over the registration message."),
|
|
277
|
+
timestamp: z.string().describe("ISO 8601 timestamp used in the signed message (within an hour of the call)."),
|
|
278
|
+
schema_url: z.string().optional().describe("Optional JSON Schema URL enabling the schema-conformance score."),
|
|
279
|
+
}, (a) => call("POST", REGISTRY_URL, "/tools/register", {
|
|
280
|
+
json: {
|
|
281
|
+
name: a.name,
|
|
282
|
+
endpointUrl: a.endpoint_url,
|
|
283
|
+
protocol: a.protocol,
|
|
284
|
+
category: a.category,
|
|
285
|
+
description: a.description,
|
|
286
|
+
ownerWallet: a.owner_wallet,
|
|
287
|
+
signature: a.signature,
|
|
288
|
+
timestamp: a.timestamp,
|
|
289
|
+
schemaUrl: a.schema_url,
|
|
290
|
+
},
|
|
291
|
+
}));
|
|
292
|
+
// ---- Wicked Identity (reverse-CAPTCHA / Know-Your-Agent) ----
|
|
293
|
+
// Two onboarding paths. A real wallet signs nonces (this server never signs).
|
|
294
|
+
// An agent with NO wallet or signing ability uses a sandbox identity:
|
|
295
|
+
// identity_sandbox_register, then pass sandbox_token instead of nonce/signature.
|
|
296
|
+
const idWallet = z.string().describe("Your 0x wallet address on Base, or the sandbox wallet from identity_sandbox_register.");
|
|
297
|
+
const idAuth = {
|
|
298
|
+
wallet: idWallet,
|
|
299
|
+
nonce: z.string().optional().describe("Real wallet: nonce from a FRESH identity_get_nonce call. Omit for sandbox."),
|
|
300
|
+
signature: z.string().optional().describe("Real wallet: 0x-prefixed ECDSA signature over that nonce's message. Omit for sandbox."),
|
|
301
|
+
sandbox_token: z.string().optional().describe("Sandbox identity: the sandbox_token from identity_sandbox_register. Use INSTEAD of nonce/signature."),
|
|
302
|
+
};
|
|
303
|
+
tool("identity_sandbox_register", "[Wicked Identity] START HERE if you have no wallet or cannot sign messages. Returns a sandbox wallet + sandbox_token (no signing, no body). Then call identity_get_challenge and identity_submit_response with wallet + sandbox_token. Sandbox results are for trying the service: signed with a separate key (identity_jwks sandbox=true) and never reported as verified. Use a real wallet for an accountable identity. Limit: 10 per IP per hour.", {}, () => call("POST", IDENTITY_URL, "/sandbox/register"));
|
|
304
|
+
tool("identity_get_test_key", "[Wicked Identity] Get a free self-serve test API key (7-day expiry, 20 requests/min) to use as api_key on identity_status instead of paying via x402. Limit: 5 per IP per day.", { label: z.string().optional().describe("Optional name for this key.") }, ({ label }) => call("POST", IDENTITY_URL, "/keys/test", { json: label ? { label } : {} }));
|
|
305
|
+
tool("identity_get_nonce", "[Wicked Identity] Real-wallet path, step 1: a one-time nonce + message to sign. Every signed identity call needs a fresh nonce (single-use, 5-minute TTL). Not needed for sandbox identities.", { wallet: idWallet }, ({ wallet }) => call("GET", IDENTITY_URL, `/agents/${addr(wallet)}/nonce`));
|
|
306
|
+
tool("identity_register", "[Wicked Identity] Register a REAL wallet (no stake required). Needs your signature over identity_get_nonce's message; this tool never signs. Sandbox identities are registered by identity_sandbox_register instead.", { wallet: idWallet, nonce: z.string().describe("Nonce from a FRESH identity_get_nonce call."), signature: z.string().describe("0x-prefixed ECDSA signature over that nonce's message.") }, ({ wallet, nonce, signature }) => call("POST", IDENTITY_URL, "/agents/register", { json: { wallet_address: wallet, nonce, signature } }));
|
|
307
|
+
tool("identity_get_challenge", "[Wicked Identity] Request a real time-boxed liveness challenge (constrained-generation task, 30s budget). The clock starts when this returns: answer in ONE pass via identity_submit_response before `expires_at`. Real wallet: needs a fresh signed nonce (fetch + sign the nonce for the NEXT call before calling this). Sandbox: just wallet + sandbox_token.", idAuth, ({ wallet, nonce, signature, sandbox_token }) => call("POST", IDENTITY_URL, "/verify/challenge", { json: { wallet_address: wallet, nonce, signature, sandbox_token } }));
|
|
308
|
+
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. Real wallet: needs a DIFFERENT fresh nonce than identity_get_challenge used. Sandbox: wallet + sandbox_token.", {
|
|
309
|
+
...idAuth,
|
|
310
|
+
challenge_id: z.string().describe("The challenge_id from identity_get_challenge."),
|
|
311
|
+
response_text: z.string().describe("Your answer exactly as the instructions specify: raw text only."),
|
|
312
|
+
}, ({ wallet, nonce, signature, sandbox_token, challenge_id, response_text }) => call("POST", IDENTITY_URL, "/verify/response", { json: { wallet_address: wallet, nonce, signature, sandbox_token, challenge_id, response_text } }));
|
|
313
|
+
tool("identity_status", "[Wicked Identity] Does a wallet currently hold a valid, unexpired assertion? No signature needed. Pays via x402, or pass api_key (get one free from identity_get_test_key) / set IDENTITY_API_KEY. Sandbox wallets are never `verified`; they report sandbox: true and sandbox_verified instead.", { wallet, api_key: z.string().optional().describe("Optional x-api-key, e.g. from identity_get_test_key.") }, ({ wallet, api_key }) => call("GET", IDENTITY_URL, `/verify/status/${addr(wallet)}`, { headers: keyHeader(api_key || IDENTITY_API_KEY) }));
|
|
314
|
+
tool("identity_jwks", "[Wicked Identity] Public RS256 keys to verify an assertion_token locally. Free. Set sandbox=true for the separate key set that signs sandbox assertions.", { sandbox: z.boolean().optional().describe("true = the sandbox key set (issuer wicked-identity-sandbox).") }, ({ sandbox }) => call("GET", IDENTITY_URL, sandbox ? "/.well-known/jwks-sandbox.json" : "/.well-known/jwks.json"));
|
|
315
|
+
// ---- Wicked Sanity (hallucination / eval check) ----
|
|
316
|
+
const contextShape = z
|
|
317
|
+
.union([z.string(), z.array(z.string())])
|
|
318
|
+
.optional()
|
|
319
|
+
.describe('Source text the claim(s) must be faithful to. Required for mode "grounded".');
|
|
320
|
+
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).');
|
|
321
|
+
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 }));
|
|
322
|
+
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 }));
|
|
323
|
+
// ---- Wicked Memory (wallet-scoped, signature per call) ----
|
|
324
|
+
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.', {
|
|
325
|
+
operation: z.string().describe('"store", "search", "get", "update", "history", "delete" or "deletions".'),
|
|
326
|
+
wallet: z.string().describe("Your agent's 0x wallet address (memories are scoped to it)."),
|
|
327
|
+
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>"}.'),
|
|
328
|
+
}, async ({ operation, wallet, params }) => {
|
|
329
|
+
let built;
|
|
330
|
+
try {
|
|
331
|
+
built = memoryBuild(operation, params);
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
return { error: err.message, source: "mcp", http_status: 400 };
|
|
335
|
+
}
|
|
336
|
+
const timestamp = String(Math.floor(Date.now() / 1000));
|
|
337
|
+
const nonce = randomBytes(16).toString("hex");
|
|
338
|
+
return {
|
|
339
|
+
message_to_sign: memoryMessage(wallet, built.method, built.path, timestamp, nonce, built.body),
|
|
340
|
+
timestamp,
|
|
341
|
+
nonce,
|
|
342
|
+
operation,
|
|
343
|
+
wallet,
|
|
344
|
+
request: { method: built.method, path: built.path },
|
|
345
|
+
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.`,
|
|
346
|
+
};
|
|
347
|
+
});
|
|
348
|
+
const memWallet = z.string().describe("Your agent's 0x wallet address.");
|
|
349
|
+
const memId = z.string().describe("The memory's UUID.");
|
|
350
|
+
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.", {
|
|
351
|
+
wallet: memWallet,
|
|
352
|
+
content: z.string().describe("Text to remember (max 16 KB)."),
|
|
353
|
+
...sigShape,
|
|
354
|
+
tags: z.array(z.string()).optional().describe("Up to 20 short tags."),
|
|
355
|
+
metadata: z.record(z.string(), z.unknown()).optional().describe("Free-form JSON object (max 8 KB)."),
|
|
356
|
+
source: z.string().optional().describe("Which agent/session wrote this."),
|
|
357
|
+
}, (a) => memoryCall("store", a.wallet, { content: a.content, tags: a.tags, metadata: a.metadata, source: a.source }, a));
|
|
358
|
+
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.", {
|
|
359
|
+
wallet: memWallet,
|
|
360
|
+
q: z.string().describe("What to look for, in natural language."),
|
|
361
|
+
...sigShape,
|
|
362
|
+
limit: z.coerce.number().optional().describe("1-50, default 10."),
|
|
363
|
+
tags: z.array(z.string()).optional().describe("Only memories having ALL of these tags."),
|
|
364
|
+
created_after: z.string().optional().describe("ISO-8601 lower bound."),
|
|
365
|
+
created_before: z.string().optional().describe("ISO-8601 upper bound."),
|
|
366
|
+
as_of: z.string().optional().describe("ISO-8601 instant; search memory as it stood then."),
|
|
367
|
+
include_superseded: z.coerce.boolean().optional().describe("Also search old, superseded versions."),
|
|
368
|
+
payment_signature: z.string().optional().describe("x402 payment payload (base64) for the keyless path."),
|
|
369
|
+
}, (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));
|
|
370
|
+
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));
|
|
371
|
+
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).", {
|
|
372
|
+
wallet: memWallet,
|
|
373
|
+
memory_id: z.string().describe("UUID of the CURRENT version to supersede."),
|
|
374
|
+
...sigShape,
|
|
375
|
+
content: z.string().optional().describe("New text (re-embedded). Provide at least one of content/tags/metadata/source."),
|
|
376
|
+
tags: z.array(z.string()).optional().describe("Replacement tags."),
|
|
377
|
+
metadata: z.record(z.string(), z.unknown()).optional().describe("Replacement metadata object."),
|
|
378
|
+
source: z.string().optional().describe("New source label."),
|
|
379
|
+
}, (a) => memoryCall("update", a.wallet, { memory_id: a.memory_id, content: a.content, tags: a.tags, metadata: a.metadata, source: a.source }, a));
|
|
380
|
+
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));
|
|
381
|
+
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.', {
|
|
382
|
+
wallet: memWallet,
|
|
383
|
+
memory_id: z.string().describe("UUID of any version in the chain."),
|
|
384
|
+
...sigShape,
|
|
385
|
+
scope: z.string().optional().describe('"chain" (all versions, default) or "version".'),
|
|
386
|
+
reason: z.string().optional().describe("Optional audit-log note (max 200 chars)."),
|
|
387
|
+
}, (a) => memoryCall("delete", a.wallet, { memory_id: a.memory_id, scope: a.scope ?? "chain", reason: a.reason }, a));
|
|
388
|
+
tool("memory_deletions", "[Wicked Memory] Your deletion audit log (ids, times, reasons; never content).", { wallet: memWallet, ...sigShape }, (a) => memoryCall("deletions", a.wallet, {}, a));
|
|
389
|
+
// ---- x402 Paywall (tenant API; needs PAYWALL_API_KEY except signup) ----
|
|
390
|
+
const routeShape = {
|
|
391
|
+
price: z.string().describe('Decimal USD amount, e.g. "0.01" or "$0.01".'),
|
|
392
|
+
network: z.string().describe("CAIP-2 id: Base mainnet eip155:8453, Base Sepolia eip155:84532."),
|
|
393
|
+
payTo: z.string().describe("Your payout wallet (0x…). Settlement pays here directly; nothing is custodied."),
|
|
394
|
+
method: z.enum(["GET", "POST"]).optional().describe("Default GET."),
|
|
395
|
+
path: z.string().optional().describe('Route path, e.g. "/weather".'),
|
|
396
|
+
asset: z.string().optional().describe("Payment token contract; defaults to USDC on the network."),
|
|
397
|
+
description: z.string().optional().describe("Up to 200 chars: letters, digits, space . _ -"),
|
|
398
|
+
upstreamUrl: z.string().optional().describe("Your https backend. Omit for the demo confirmation payload."),
|
|
399
|
+
};
|
|
400
|
+
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.", {
|
|
401
|
+
name: z.string().describe("Letters, digits, space . _ - (max 100)."),
|
|
402
|
+
slug: z.string().describe("Lowercase a-z0-9 and hyphens, 2-40 chars; becomes your mount path prefix /{slug}."),
|
|
403
|
+
}, ({ name, slug }) => call("POST", PAYWALL_URL, "/signup", { json: { name, slug } }));
|
|
404
|
+
tool("paywall_get_tenant", "[x402 Paywall] Your tenant record (needs PAYWALL_API_KEY).", {}, () => call("GET", PAYWALL_URL, "/tenants/me", { headers: bearer(PAYWALL_API_KEY) }));
|
|
405
|
+
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) }));
|
|
406
|
+
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 }));
|
|
407
|
+
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 } }));
|
|
408
|
+
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).", {
|
|
409
|
+
id: z.string().describe("Route id from paywall_list_routes."),
|
|
410
|
+
price: routeShape.price.optional(),
|
|
411
|
+
network: routeShape.network.optional(),
|
|
412
|
+
payTo: routeShape.payTo.optional(),
|
|
413
|
+
method: routeShape.method,
|
|
414
|
+
path: routeShape.path,
|
|
415
|
+
asset: routeShape.asset,
|
|
416
|
+
description: routeShape.description,
|
|
417
|
+
upstreamUrl: routeShape.upstreamUrl,
|
|
418
|
+
}, ({ id, ...patch }) => call("PATCH", PAYWALL_URL, `/tenants/me/routes/${encodeURIComponent(id)}`, { headers: bearer(PAYWALL_API_KEY), json: patch }));
|
|
419
|
+
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) }));
|
|
420
|
+
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 } }));
|
|
421
|
+
return n;
|
|
422
|
+
}
|
|
423
|
+
// Exposed for tests: deterministic builders used by memory_prepare.
|
|
424
|
+
export const __test = { memoryBuild, memoryMessage };
|
package/dist/usage.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const startedAt = Date.now();
|
|
2
|
+
const counts = new Map();
|
|
3
|
+
export const knownTools = new Set();
|
|
4
|
+
export const usageHooks = {
|
|
5
|
+
onRegister: (name) => knownTools.add(name),
|
|
6
|
+
onToolCall: (name, ms, isError) => {
|
|
7
|
+
const c = counts.get(name) ?? { calls: 0, errors: 0, totalMs: 0 };
|
|
8
|
+
c.calls++;
|
|
9
|
+
c.totalMs += ms;
|
|
10
|
+
if (isError)
|
|
11
|
+
c.errors++;
|
|
12
|
+
counts.set(name, c);
|
|
13
|
+
console.log(JSON.stringify({ evt: "tool_call", tool: name, error: isError, ms }));
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
export function usageSnapshot() {
|
|
17
|
+
const tools = [...counts.entries()]
|
|
18
|
+
.map(([tool, c]) => ({ tool, calls: c.calls, errors: c.errors, avg_ms: Math.round(c.totalMs / c.calls) }))
|
|
19
|
+
.sort((a, b) => b.calls - a.calls);
|
|
20
|
+
return {
|
|
21
|
+
since: new Date(startedAt).toISOString(),
|
|
22
|
+
note: "In-memory since last deploy; durable history is in the service logs.",
|
|
23
|
+
total_calls: tools.reduce((n, t) => n + t.calls, 0),
|
|
24
|
+
tools,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// Wrap server.registerTool so every handler reports to the hooks. Must run
|
|
28
|
+
// before any tools are registered.
|
|
29
|
+
export function instrument(server, hooks) {
|
|
30
|
+
const original = server.registerTool.bind(server);
|
|
31
|
+
server.registerTool = (name, config, handler) => {
|
|
32
|
+
hooks.onRegister?.(name);
|
|
33
|
+
return original(name, config, async (...args) => {
|
|
34
|
+
const t0 = Date.now();
|
|
35
|
+
let isError = true;
|
|
36
|
+
try {
|
|
37
|
+
const result = await handler(...args);
|
|
38
|
+
isError = Boolean(result?.isError);
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
hooks.onToolCall?.(name, Date.now() - t0, isError);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wickedlabs/wicked-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.5.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. 75 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": "WickedLabs",
|
|
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
|
+
"@sentry/node": "^11.4.0",
|
|
50
|
+
"viem": "^2.55.2",
|
|
51
|
+
"x402-fetch": "^1.2.0",
|
|
52
|
+
"zod": "^4.4.3"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/node": "^22.20.1",
|
|
56
|
+
"typescript": "^5.9.3"
|
|
57
|
+
},
|
|
58
|
+
"repository": {
|
|
59
|
+
"type": "git",
|
|
60
|
+
"url": "git+https://github.com/choaticpixels/wicked-mcp.git"
|
|
61
|
+
},
|
|
62
|
+
"mcpName": "io.github.choaticpixels/wicked-mcp"
|
|
63
|
+
}
|