adk-ts/apps/docs/scripts/fetch-mcp-tools.ts

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);
});