317 lines
8.3 KiB
TypeScript
317 lines
8.3 KiB
TypeScript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
interface McpServer {
|
|
slug: string;
|
|
name: string;
|
|
package: string;
|
|
skip?: boolean;
|
|
skipReason?: string;
|
|
}
|
|
|
|
interface McpToolData {
|
|
name: string;
|
|
description: string;
|
|
inputSchema: Record<string, unknown>;
|
|
}
|
|
|
|
interface McpServerResult {
|
|
name: string;
|
|
package: string;
|
|
tools: McpToolData[];
|
|
error: string | null;
|
|
}
|
|
|
|
const DUMMY_ENV: Record<string, string> = {
|
|
PATH: process.env.PATH || "",
|
|
WALLET_PRIVATE_KEY:
|
|
"0x0000000000000000000000000000000000000000000000000000000000000001",
|
|
ATP_WALLET_PRIVATE_KEY:
|
|
"0x0000000000000000000000000000000000000000000000000000000000000001",
|
|
ATP_API_KEY: "dummy",
|
|
TELEGRAM_BOT_TOKEN: "000000000:AAFdummytokenvalue",
|
|
DISCORD_TOKEN: "dummy_discord_token",
|
|
ACCOUNT_ID: "dummy.testnet",
|
|
ACCOUNT_KEY:
|
|
"ed25519:5fGF5mfbLNTPGLCCz1NQXV6QXMJ9GkSbEMDdkNMpDqP6bKqxqvNBQtbZ1234567890abcdef",
|
|
NEAR_NETWORK_ID: "testnet",
|
|
FUNDER_ADDRESS: "0x0000000000000000000000000000000000000001",
|
|
POLYMARKET_PRIVATE_KEY:
|
|
"0x0000000000000000000000000000000000000000000000000000000000000001",
|
|
DEBANK_API_KEY: "dummy_debank_api_key",
|
|
DEFILLAMA_API_KEY: "dummy_defillama_api_key",
|
|
KALSHI_API_KEY: "dummy_kalshi_api_key",
|
|
KALSHI_PRIVATE_KEY_PEM:
|
|
"-----BEGIN RSA PRIVATE KEY-----\nMIIBogIBAAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf9Cnzj4p4WGeKLs1Pt8Qu\nKUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQJAIJLixBy2qpFoS4DSmoEm\no3qGy0t6z09AIJtH+5OeRV1be+N4cDYJKffGzDa88vQENZiRm0GRq6a+HPGQMd2k\nTQIhAKMSvzIBnni7ot/OSie2TmJLY4SwTQAevXysE2RbFDYdAiEBvHCS1mknvGuV\nIU3IYO/vtXz9Cwvq6gR4SMbQOBW5p0UCIHewg9cLG/u8Rx85cxCZfXcq5/v0gWq3\nAAAAAAAAAAAA\n-----END RSA PRIVATE KEY-----",
|
|
OPINION_API_KEY: "dummy_opinion_api_key",
|
|
};
|
|
|
|
const MCP_SERVERS: McpServer[] = [
|
|
{
|
|
slug: "abi",
|
|
name: "ABI MCP Client",
|
|
package: "@iqai/mcp-abi",
|
|
skip: true,
|
|
skipReason:
|
|
"Tools are dynamically generated from a user-provided contract ABI",
|
|
},
|
|
{ slug: "atp", name: "ATP MCP Client", package: "@iqai/mcp-atp" },
|
|
{ slug: "bamm", name: "BAMM MCP Client", package: "@iqai/mcp-bamm" },
|
|
{
|
|
slug: "fraxlend",
|
|
name: "Fraxlend MCP Client",
|
|
package: "@iqai/mcp-fraxlend",
|
|
},
|
|
{ slug: "iqwiki", name: "IQWiki MCP Client", package: "@iqai/mcp-iqwiki" },
|
|
{
|
|
slug: "near-agent",
|
|
name: "NEAR Agent MCP Client",
|
|
package: "@iqai/mcp-near-agent",
|
|
},
|
|
{
|
|
slug: "near-intents",
|
|
name: "Near Intents Swaps MCP Client",
|
|
package: "@iqai/mcp-near-intent-swaps",
|
|
},
|
|
{ slug: "odos", name: "ODOS MCP Client", package: "@iqai/mcp-odos" },
|
|
{
|
|
slug: "telegram",
|
|
name: "Telegram MCP Client",
|
|
package: "@iqai/mcp-telegram",
|
|
},
|
|
{
|
|
slug: "discord",
|
|
name: "Discord MCP Client",
|
|
package: "@iqai/mcp-discord",
|
|
},
|
|
{
|
|
slug: "coingecko",
|
|
name: "CoinGecko MCP Client",
|
|
package: "https://mcp.api.coingecko.com/mcp",
|
|
},
|
|
{
|
|
slug: "coingecko-pro",
|
|
name: "CoinGecko Pro MCP Client",
|
|
package: "https://mcp.pro-api.coingecko.com/mcp",
|
|
skip: true,
|
|
skipReason:
|
|
"Remote MCP endpoint requires CoinGecko Pro API key authorization",
|
|
},
|
|
{ slug: "upbit", name: "Upbit MCP Client", package: "@iqai/mcp-upbit" },
|
|
{
|
|
slug: "polymarket",
|
|
name: "Polymarket MCP Client",
|
|
package: "@iqai/mcp-polymarket",
|
|
},
|
|
{
|
|
slug: "debank",
|
|
name: "DeBank MCP Client",
|
|
package: "@iqai/mcp-debank",
|
|
},
|
|
{
|
|
slug: "defillama",
|
|
name: "DefiLlama MCP Client",
|
|
package: "@iqai/defillama-mcp",
|
|
},
|
|
{
|
|
slug: "kalshi",
|
|
name: "Kalshi MCP Client",
|
|
package: "@iqai/mcp-kalshi",
|
|
},
|
|
{
|
|
slug: "limitless",
|
|
name: "Limitless MCP Client",
|
|
package: "@iqai/mcp-limitless",
|
|
},
|
|
{
|
|
slug: "opinion",
|
|
name: "Opinion MCP Client",
|
|
package: "@iqai/mcp-opinion",
|
|
},
|
|
{
|
|
slug: "filesystem",
|
|
name: "Filesystem MCP Client",
|
|
package: "@modelcontextprotocol/server-filesystem",
|
|
},
|
|
{
|
|
slug: "memory",
|
|
name: "Memory MCP Client",
|
|
package: "@modelcontextprotocol/server-memory",
|
|
},
|
|
{
|
|
slug: "playwright",
|
|
name: "Playwright MCP Client",
|
|
package: "@playwright/mcp",
|
|
},
|
|
{
|
|
slug: "sequential-thinking",
|
|
name: "Sequential Thinking MCP Client",
|
|
package: "@modelcontextprotocol/server-sequential-thinking",
|
|
},
|
|
{
|
|
slug: "notion",
|
|
name: "Notion MCP Client",
|
|
package: "@notionhq/notion-mcp-server",
|
|
},
|
|
];
|
|
|
|
const TIMEOUT_MS = 60_000;
|
|
|
|
function withTimeout<T>(promise: Promise<T>, message: string): Promise<T> {
|
|
const ac = new AbortController();
|
|
const timer = setTimeout(() => ac.abort(), TIMEOUT_MS);
|
|
return Promise.race([
|
|
promise.then((result) => {
|
|
clearTimeout(timer);
|
|
return result;
|
|
}),
|
|
new Promise<never>((_, reject) => {
|
|
ac.signal.addEventListener("abort", () => reject(new Error(message)));
|
|
}),
|
|
]);
|
|
}
|
|
|
|
function isUrl(value: string): boolean {
|
|
try {
|
|
const url = new URL(value);
|
|
return url.protocol === "http:" || url.protocol === "https:";
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function fetchToolsForServer(
|
|
server: McpServer,
|
|
): Promise<McpServerResult> {
|
|
if (server.skip) {
|
|
return {
|
|
name: server.name,
|
|
package: server.package,
|
|
tools: [],
|
|
error: server.skipReason || "Skipped",
|
|
};
|
|
}
|
|
|
|
const args = isUrl(server.package)
|
|
? ["mcp-remote@latest", server.package]
|
|
: [server.package];
|
|
|
|
const stderrChunks: Buffer[] = [];
|
|
const transport = new StdioClientTransport({
|
|
command: "pnpm",
|
|
args: ["dlx", ...args],
|
|
env: DUMMY_ENV,
|
|
stderr: "pipe",
|
|
});
|
|
|
|
transport.stderr?.on("data", (chunk: Buffer) => {
|
|
stderrChunks.push(chunk);
|
|
});
|
|
|
|
const client = new Client(
|
|
{ name: `docs-fetcher-${server.slug}`, version: "0.0.1" },
|
|
{ capabilities: {} },
|
|
);
|
|
|
|
try {
|
|
await withTimeout(client.connect(transport), "Connection timeout");
|
|
const result = await withTimeout(client.listTools(), "listTools timeout");
|
|
|
|
const tools: McpToolData[] = (result.tools || []).map((tool) => ({
|
|
name: tool.name,
|
|
description: tool.description || "",
|
|
inputSchema: tool.inputSchema || { type: "object", properties: {} },
|
|
}));
|
|
|
|
return {
|
|
name: server.name,
|
|
package: server.package,
|
|
tools,
|
|
error: null,
|
|
};
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
const stderrOutput = Buffer.concat(stderrChunks).toString().trim();
|
|
if (stderrOutput) {
|
|
console.error(` Failed: ${message}\n stderr: ${stderrOutput}`);
|
|
} else {
|
|
console.error(` Failed: ${message}`);
|
|
}
|
|
return {
|
|
name: server.name,
|
|
package: server.package,
|
|
tools: [],
|
|
error: message,
|
|
};
|
|
} finally {
|
|
try {
|
|
await client.close();
|
|
} catch {}
|
|
try {
|
|
await transport.close();
|
|
} catch {}
|
|
}
|
|
}
|
|
|
|
const CONCURRENCY = 5;
|
|
|
|
async function main() {
|
|
const servers: Record<string, McpServerResult> = {};
|
|
|
|
// Fetch remote endpoints (CoinGecko) sequentially to avoid mcp-remote connection issues
|
|
const remoteServers = MCP_SERVERS.filter((s) => isUrl(s.package));
|
|
const localServers = MCP_SERVERS.filter((s) => !isUrl(s.package));
|
|
|
|
// Process remote servers one at a time
|
|
for (const server of remoteServers) {
|
|
console.log(`Fetching tools for ${server.slug}...`);
|
|
const result = await fetchToolsForServer(server);
|
|
console.log(
|
|
` -> ${server.slug}: ${result.tools.length} tools${result.error ? ` (${result.error})` : ""}`,
|
|
);
|
|
servers[server.slug] = result;
|
|
// Small delay between remote fetches to avoid connection state issues
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
}
|
|
|
|
// Process local servers in batches
|
|
for (let i = 0; i < localServers.length; i += CONCURRENCY) {
|
|
const batch = localServers.slice(i, i + CONCURRENCY);
|
|
const results = await Promise.all(
|
|
batch.map(async (server) => {
|
|
console.log(`Fetching tools for ${server.slug}...`);
|
|
const result = await fetchToolsForServer(server);
|
|
console.log(
|
|
` -> ${server.slug}: ${result.tools.length} tools${result.error ? ` (${result.error})` : ""}`,
|
|
);
|
|
return { slug: server.slug, result };
|
|
}),
|
|
);
|
|
|
|
for (const { slug, result } of results) {
|
|
servers[slug] = result;
|
|
}
|
|
}
|
|
|
|
const output = {
|
|
generatedAt: new Date().toISOString(),
|
|
servers,
|
|
};
|
|
|
|
const outputPath = path.join(__dirname, "../data/mcp-tools.json");
|
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));
|
|
console.log(`\nWrote tool data to ${outputPath}`);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error("Fatal error:", err);
|
|
process.exit(1);
|
|
});
|