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