@agentkey/mcp 0.0.4 → 0.0.6

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/dist/index.js CHANGED
@@ -41,12 +41,13 @@ const fs = __importStar(require("fs"));
41
41
  const path = __importStar(require("path"));
42
42
  const os = __importStar(require("os"));
43
43
  const readline = __importStar(require("readline"));
44
+ const dynamic_js_1 = require("./dynamic.js");
44
45
  // --- Configuration ---
45
46
  const AGENTKEY_BASE_URL = process.env.AGENTKEY_BASE_URL || "https://api.agentkey.app";
46
47
  const AGENTKEY_API_KEY = process.env.AGENTKEY_API_KEY || "";
47
48
  // --- CLI: --list-tools ---
48
49
  if (process.argv.includes("--list-tools")) {
49
- console.log(`AgentKey MCP Server v0.0.4
50
+ console.log(`AgentKey MCP Server v0.0.6
50
51
 
51
52
  Tools:
52
53
  agentkey_search Search the web, news, images, videos, or places
@@ -54,7 +55,9 @@ Tools:
54
55
  agentkey_scrape Scrape a webpage and extract content as markdown/html/text
55
56
  Providers: Firecrawl, Jina Reader, ScrapeNinja
56
57
  agentkey_social Query social media (search, posts, comments, user profiles)
57
- Platforms: Twitter/X, Reddit
58
+ Platforms: Twitter, Reddit, Xiaohongshu, Instagram, Zhihu, TikTok, Douyin, Bilibili, Weibo, Threads, YouTube
59
+ agentkey_crypto Query blockchain & crypto data (prices, NFTs, balances, txs, ENS)
60
+ Provider: Chainbase
58
61
 
59
62
  Environment:
60
63
  AGENTKEY_BASE_URL API base URL (default: https://api.agentkey.app)
@@ -273,7 +276,7 @@ else {
273
276
  ok: false,
274
277
  status: 429,
275
278
  data: {
276
- error: "Rate limit exceeded. Please wait a moment and try again, or upgrade your plan at https://agentkey.app",
279
+ error: "Rate limit exceeded. Do NOT retry immediately — tell the user the service is busy and suggest trying again in a few seconds.",
277
280
  },
278
281
  };
279
282
  }
@@ -292,320 +295,756 @@ else {
292
295
  ],
293
296
  };
294
297
  }
298
+ /**
299
+ * Compact search results to reduce token consumption.
300
+ * Strips fields that are empty/null/zero and removes indentation.
301
+ */
302
+ function compactResult(data) {
303
+ const compact = JSON.parse(JSON.stringify(data, (_, v) => v === null || v === "" || v === 0 || (Array.isArray(v) && v.length === 0) ? undefined : v));
304
+ return {
305
+ content: [
306
+ { type: "text", text: JSON.stringify(compact) },
307
+ ],
308
+ };
309
+ }
295
310
  // --- MCP Server ---
296
311
  const server = new mcp_js_1.McpServer({
297
312
  name: "AgentKey",
298
- version: "0.0.4",
313
+ version: "0.0.6",
314
+ description: `AgentKey provides 4 tools: search, scrape, social, crypto.
315
+
316
+ MOST IMPORTANT RULE — USE YOUR OWN KNOWLEDGE FIRST:
317
+ - If you can answer from your training data (facts, concepts, history, definitions, explanations), do so WITHOUT calling any tool.
318
+ - Only call a tool when the user needs REAL-TIME or RECENT data that is beyond your knowledge cutoff.
319
+ - Examples that need NO tool: "what is DeFi", "explain OAuth", "compare React vs Vue", "who is Vitalik Buterin", "what is proof of stake".
320
+ - Examples that NEED a tool: "BTC price now", "latest news about OpenAI", "trending on Twitter today", "current top holders of USDT".
321
+
322
+ DECISION TREE (only when a tool IS needed):
323
+ 1. Crypto prices, on-chain data, wallets, NFTs → agentkey_crypto
324
+ 2. Social media content (Twitter, Reddit, Instagram, etc.) → agentkey_social
325
+ 3. Full content of a specific URL → agentkey_scrape
326
+ 4. Web search, news, general real-time info → agentkey_search
327
+
328
+ GLOBAL RULES:
329
+ - Call at most ONE tool per turn. Never batch multiple tool calls in parallel.
330
+ - Analyze results before deciding if another call is needed.
331
+ - If a tool returns useful results, answer the user — do not call another tool "for more context".
332
+ - Prefer the most specific tool. agentkey_crypto > agentkey_search for "BTC price". agentkey_social > agentkey_search for "what people think about X on Twitter".`,
299
333
  });
300
- // Search tool
301
- server.tool("agentkey_search", `Search the web, news, images, videos, or places via AgentKey unified API.
334
+ // --- Built-in (static) tool definitions ---
335
+ // Only used as fallback when the REST API server is unreachable for dynamic loading.
336
+ function registerBuiltinTools() {
337
+ // Search tool
338
+ server.tool("agentkey_search", `Search the web, news, images, videos, or places via AgentKey unified API.
302
339
  Aggregates results from Serper, Tavily, Brave, and Perplexity with automatic failover.
303
340
 
304
- IMPORTANT: Do NOT use this tool for Twitter/X or Reddit queries. Use the "agentkey_social" tool instead for any social media content, discussions, tweets, or Reddit posts.
341
+ THINK BEFORE CALLING use your own knowledge first:
342
+ - If you can answer from your training data (facts, concepts, history, definitions), do so WITHOUT calling this tool.
343
+ - Only call when the user needs REAL-TIME or RECENT information beyond your knowledge cutoff.
344
+ - Examples that do NOT need search: "what is kubernetes", "explain OAuth 2.0", "compare React vs Vue".
345
+ - Examples that NEED search: "latest news about OpenAI", "current stock price of AAPL", "what happened in tech this week".
346
+ - If the user explicitly says "search for" or "look up", respect that intent and call.
347
+
348
+ ROUTING — use the right tool:
349
+ - Social media queries (Twitter, Reddit, Instagram, etc.) → use agentkey_social instead.
350
+ - Crypto prices or on-chain data → use agentkey_crypto instead.
351
+
352
+ STRICT RULE — MAXIMUM ONE SEARCH CALL AT A TIME:
353
+ - You MUST call this tool at most ONCE per turn. Wait for results, analyze them, and only then decide if another search is needed.
354
+ - NEVER batch multiple search calls in parallel.
355
+
356
+ QUERY OPTIMIZATION:
357
+ - ONE well-crafted query beats multiple narrow queries. For "latest news about X", search "X" with type="news" and time_range="week" — do NOT split into "X funding", "X partnership", "X product launch" as separate calls.
358
+ - Do NOT search the same topic from different angles or in multiple languages. Use one comprehensive query.
359
+ - If you already have a URL from search results and need details, use agentkey_scrape instead of searching again.
360
+ - Default num to 5. Use 10 only when the user explicitly needs comprehensive results.
305
361
 
306
362
  Preferences:
307
- - Default provider to "auto" for best speed and reliability. Use a specific provider only when the user explicitly requests it.
308
- - Set "lang" based on the language of the user's query (e.g. Chinese→"zh", English→"en", Japanese→"ja"). The user can also explicitly override.
309
- - Default num to 10 unless the user specifies otherwise.
363
+ - Default provider to "auto" for best speed and reliability.
364
+ - Set "lang" based on the language of the user's query (e.g. Chinese→"zh", English→"en", Japanese→"ja").
310
365
  - Default type to "web" unless the user asks for news, images, videos, or places.
311
366
  - Use time_range when the user asks for "recent", "latest", "this week", "today" etc.
312
367
  - Use include_domains/exclude_domains when the user wants results from or excluding specific sites.`, {
313
- query: zod_1.z.string().describe("Search query"),
314
- provider: zod_1.z
315
- .enum(["auto", "serper", "tavily", "brave", "perplexity"])
316
- .optional()
317
- .describe("Search provider. Default 'auto' (fastest & cheapest). Only specify when user explicitly asks."),
318
- type: zod_1.z
319
- .enum(["web", "news", "images", "videos", "places"])
320
- .optional()
321
- .describe("Search type. Default 'web'. Use 'news' for recent articles, 'images' for pictures, 'videos' for video content, 'places' for locations/businesses."),
322
- search_depth: zod_1.z
323
- .enum(["basic", "advanced"])
324
- .optional()
325
- .describe("Search depth. 'basic' for fast results, 'advanced' for more thorough and detailed results. Default 'basic'."),
326
- num: zod_1.z
327
- .coerce.number()
328
- .min(1)
329
- .max(50)
330
- .optional()
331
- .describe("Number of results, default 10"),
332
- country: zod_1.z
333
- .string()
334
- .optional()
335
- .describe("Country code (ISO 3166-1 alpha-2) to localize results, e.g. 'us', 'cn', 'jp'"),
336
- lang: zod_1.z
337
- .string()
338
- .optional()
339
- .describe("Language code for results. Auto-detect from user's query language: Chinese→'zh', English→'en', Japanese→'ja', etc. User can override."),
340
- time_range: zod_1.z
341
- .enum(["day", "week", "month", "year"])
342
- .optional()
343
- .describe("Time range filter. Use 'day' for today, 'week' for this week, 'month' for this month, 'year' for this year. Useful for news and recent content."),
344
- include_domains: zod_1.z
345
- .array(zod_1.z.string())
346
- .optional()
347
- .describe("Only include results from these domains, e.g. ['github.com', 'arxiv.org']"),
348
- exclude_domains: zod_1.z
349
- .array(zod_1.z.string())
350
- .optional()
351
- .describe("Exclude results from these domains, e.g. ['pinterest.com', 'quora.com']"),
352
- page: zod_1.z
353
- .coerce.number()
354
- .min(1)
355
- .optional()
356
- .describe("Page number for pagination, starting from 1"),
357
- }, async ({ query, provider, type, search_depth, num, country, lang, time_range, include_domains, exclude_domains, page, }) => {
358
- const body = {
359
- query,
360
- provider: provider || "auto",
361
- };
362
- if (type)
363
- body.type = type;
364
- if (search_depth)
365
- body.search_depth = search_depth;
366
- if (num)
367
- body.num = num;
368
- if (country)
369
- body.country = country;
370
- if (lang)
371
- body.lang = lang;
372
- if (time_range)
373
- body.time_range = time_range;
374
- if (include_domains?.length)
375
- body.include_domains = include_domains;
376
- if (exclude_domains?.length)
377
- body.exclude_domains = exclude_domains;
378
- if (page)
379
- body.page = page;
380
- try {
381
- const { ok, data } = await callAPI("/v1/search", body);
382
- if (!ok) {
383
- return errorResult(`Search failed: ${data.error || JSON.stringify(data)}`);
368
+ query: zod_1.z.string().describe("Search query"),
369
+ provider: zod_1.z
370
+ .enum(["auto", "serper", "tavily", "brave", "perplexity"])
371
+ .optional()
372
+ .describe("Search provider. Default 'auto' (fastest & cheapest). Only specify when user explicitly asks."),
373
+ type: zod_1.z
374
+ .enum(["web", "news", "images", "videos", "places"])
375
+ .optional()
376
+ .describe("Search type. Default 'web'. Use 'news' for recent articles, 'images' for pictures, 'videos' for video content, 'places' for locations/businesses."),
377
+ search_depth: zod_1.z
378
+ .enum(["basic", "advanced"])
379
+ .optional()
380
+ .describe("Search depth. 'basic' for fast results, 'advanced' for more thorough and detailed results. Default 'basic'."),
381
+ num: zod_1.z
382
+ .coerce.number()
383
+ .min(1)
384
+ .max(50)
385
+ .optional()
386
+ .describe("Number of results, default 5. Use 10 only when user needs comprehensive coverage."),
387
+ country: zod_1.z
388
+ .string()
389
+ .optional()
390
+ .describe("Country code (ISO 3166-1 alpha-2) to localize results, e.g. 'us', 'cn', 'jp'"),
391
+ lang: zod_1.z
392
+ .string()
393
+ .optional()
394
+ .describe("Language code for results. Auto-detect from user's query language: Chinese→'zh', English→'en', Japanese→'ja', etc. User can override."),
395
+ time_range: zod_1.z
396
+ .enum(["day", "week", "month", "year"])
397
+ .optional()
398
+ .describe("Time range filter. Use 'day' for today, 'week' for this week, 'month' for this month, 'year' for this year. Useful for news and recent content."),
399
+ include_domains: zod_1.z
400
+ .array(zod_1.z.string())
401
+ .optional()
402
+ .describe("Only include results from these domains, e.g. ['github.com', 'arxiv.org']"),
403
+ exclude_domains: zod_1.z
404
+ .array(zod_1.z.string())
405
+ .optional()
406
+ .describe("Exclude results from these domains, e.g. ['pinterest.com', 'quora.com']"),
407
+ page: zod_1.z
408
+ .coerce.number()
409
+ .min(1)
410
+ .optional()
411
+ .describe("Page number for pagination, starting from 1"),
412
+ }, async ({ query, provider, type, search_depth, num, country, lang, time_range, include_domains, exclude_domains, page, }) => {
413
+ const body = {
414
+ query,
415
+ provider: provider || "auto",
416
+ };
417
+ if (type)
418
+ body.type = type;
419
+ if (search_depth)
420
+ body.search_depth = search_depth;
421
+ if (num)
422
+ body.num = num;
423
+ if (country)
424
+ body.country = country;
425
+ if (lang)
426
+ body.lang = lang;
427
+ if (time_range)
428
+ body.time_range = time_range;
429
+ if (include_domains?.length)
430
+ body.include_domains = include_domains;
431
+ if (exclude_domains?.length)
432
+ body.exclude_domains = exclude_domains;
433
+ if (page)
434
+ body.page = page;
435
+ try {
436
+ const { ok, data } = await callAPI("/v1/search", body);
437
+ if (!ok) {
438
+ return errorResult(`Search failed: ${data.error || JSON.stringify(data)}`);
439
+ }
440
+ const results = data.results;
441
+ const count = results?.length ?? 0;
442
+ const hint = count > 0
443
+ ? `\n\n[${count} results returned. Analyze these before making additional searches. Use agentkey_scrape only if you need the full content of a specific URL above.]`
444
+ : `\n\n[No results. Try broader keywords or a different type/time_range. Do NOT retry the same query.]`;
445
+ const compact = compactResult(data);
446
+ compact.content[0].text += hint;
447
+ return compact;
384
448
  }
385
- return textResult(data);
386
- }
387
- catch (err) {
388
- return errorResult(`Failed to connect to AgentKey server at ${AGENTKEY_BASE_URL}: ${err}`);
389
- }
390
- });
391
- // Scrape tool
392
- server.tool("agentkey_scrape", `Scrape a webpage and extract its content as markdown, HTML, text, or screenshot via AgentKey unified API.
449
+ catch (err) {
450
+ return errorResult(`Failed to connect to AgentKey server at ${AGENTKEY_BASE_URL}: ${err}`);
451
+ }
452
+ });
453
+ // Scrape tool
454
+ server.tool("agentkey_scrape", `Scrape a webpage and extract its content as markdown, HTML, text, or screenshot via AgentKey unified API.
393
455
  Aggregates Firecrawl, Jina Reader, and ScrapeNinja with automatic failover.
394
456
 
457
+ THINK BEFORE CALLING — scraping is expensive:
458
+ - Only scrape when you have a specific URL AND need its CURRENT content.
459
+ - If the user asks about a concept or topic, use agentkey_search first — do NOT jump to scraping.
460
+ - Never scrape speculatively or "just to check". You must have a clear reason to read a specific page.
461
+ - If search results already contain enough information (snippets, descriptions), do NOT scrape the source URL.
462
+
463
+ STRICT RULE — MAXIMUM ONE SCRAPE CALL AT A TIME:
464
+ - You MUST call this tool at most ONCE per turn. Do NOT batch-scrape multiple URLs in parallel.
465
+ - Scrape only the single most relevant URL. NEVER scrape 2+ pages "to be thorough".
466
+
395
467
  Preferences:
396
468
  - Default provider to "auto" (uses Jina Reader — fastest and cheapest).
397
469
  - Default format to "markdown" which is best for LLM consumption.
398
470
  - Use "include_links": true when the user wants to know what links are on the page.
399
471
  - Use CSS selectors (include_selectors/exclude_selectors) when the user wants specific parts of the page.
400
472
  - Use "screenshot" format only when the user explicitly asks to see what the page looks like.`, {
401
- url: zod_1.z
402
- .string()
403
- .describe("URL to scrape (must start with http:// or https://)"),
404
- provider: zod_1.z
405
- .enum(["auto", "firecrawl", "jina", "scrapeninja"])
406
- .optional()
407
- .describe("Scrape provider. Default 'auto' (Jina Reader). Use 'firecrawl' for JS-heavy pages or advanced extraction."),
408
- format: zod_1.z
409
- .enum(["markdown", "html", "text", "screenshot"])
410
- .optional()
411
- .describe("Output format. Default 'markdown'. Best for LLM consumption."),
412
- include_selectors: zod_1.z
413
- .array(zod_1.z.string())
414
- .optional()
415
- .describe("CSS selectors to include, e.g. ['.article', '#main-content']"),
416
- exclude_selectors: zod_1.z
417
- .array(zod_1.z.string())
418
- .optional()
419
- .describe("CSS selectors to exclude, e.g. ['.nav', '.footer', '.sidebar']"),
420
- include_links: zod_1.z
421
- .boolean()
422
- .optional()
423
- .describe("Include extracted links from the page"),
424
- include_images: zod_1.z
425
- .boolean()
426
- .optional()
427
- .describe("Include extracted image URLs from the page"),
428
- timeout: zod_1.z
429
- .coerce.number()
430
- .min(1)
431
- .max(120)
432
- .optional()
433
- .describe("Timeout in seconds, default 30"),
434
- country: zod_1.z
435
- .string()
436
- .optional()
437
- .describe("Proxy country code for geo-restricted content, e.g. 'us', 'cn'"),
438
- wait_for: zod_1.z
439
- .coerce.number()
440
- .optional()
441
- .describe("Wait milliseconds before extracting content. Useful for pages with delayed JS rendering."),
442
- }, async ({ url, provider, format, include_selectors, exclude_selectors, include_links, include_images, timeout, country, wait_for, }) => {
443
- const body = {
444
- url,
445
- provider: provider || "auto",
446
- };
447
- if (format)
448
- body.format = format;
449
- if (include_selectors?.length)
450
- body.include_selectors = include_selectors;
451
- if (exclude_selectors?.length)
452
- body.exclude_selectors = exclude_selectors;
453
- if (include_links)
454
- body.include_links = include_links;
455
- if (include_images)
456
- body.include_images = include_images;
457
- if (timeout)
458
- body.timeout = timeout;
459
- if (country)
460
- body.country = country;
461
- if (wait_for)
462
- body.wait_for = wait_for;
463
- try {
464
- const { ok, data } = await callAPI("/v1/scrape", body);
465
- if (!ok) {
466
- return errorResult(`Scrape failed: ${data.error || JSON.stringify(data)}`);
473
+ url: zod_1.z
474
+ .string()
475
+ .describe("URL to scrape (must start with http:// or https://)"),
476
+ provider: zod_1.z
477
+ .enum(["auto", "firecrawl", "jina", "scrapeninja"])
478
+ .optional()
479
+ .describe("Scrape provider. Default 'auto' (Jina Reader). Use 'firecrawl' for JS-heavy pages or advanced extraction."),
480
+ format: zod_1.z
481
+ .enum(["markdown", "html", "text", "screenshot"])
482
+ .optional()
483
+ .describe("Output format. Default 'markdown'. Best for LLM consumption."),
484
+ include_selectors: zod_1.z
485
+ .array(zod_1.z.string())
486
+ .optional()
487
+ .describe("CSS selectors to include, e.g. ['.article', '#main-content']"),
488
+ exclude_selectors: zod_1.z
489
+ .array(zod_1.z.string())
490
+ .optional()
491
+ .describe("CSS selectors to exclude, e.g. ['.nav', '.footer', '.sidebar']"),
492
+ include_links: zod_1.z
493
+ .boolean()
494
+ .optional()
495
+ .describe("Include extracted links from the page"),
496
+ include_images: zod_1.z
497
+ .boolean()
498
+ .optional()
499
+ .describe("Include extracted image URLs from the page"),
500
+ timeout: zod_1.z
501
+ .coerce.number()
502
+ .min(1)
503
+ .max(120)
504
+ .optional()
505
+ .describe("Timeout in seconds, default 30"),
506
+ country: zod_1.z
507
+ .string()
508
+ .optional()
509
+ .describe("Proxy country code for geo-restricted content, e.g. 'us', 'cn'"),
510
+ wait_for: zod_1.z
511
+ .coerce.number()
512
+ .optional()
513
+ .describe("Wait milliseconds before extracting content. Useful for pages with delayed JS rendering."),
514
+ }, async ({ url, provider, format, include_selectors, exclude_selectors, include_links, include_images, timeout, country, wait_for, }) => {
515
+ const body = {
516
+ url,
517
+ provider: provider || "auto",
518
+ };
519
+ if (format)
520
+ body.format = format;
521
+ if (include_selectors?.length)
522
+ body.include_selectors = include_selectors;
523
+ if (exclude_selectors?.length)
524
+ body.exclude_selectors = exclude_selectors;
525
+ if (include_links)
526
+ body.include_links = include_links;
527
+ if (include_images)
528
+ body.include_images = include_images;
529
+ if (timeout)
530
+ body.timeout = timeout;
531
+ if (country)
532
+ body.country = country;
533
+ if (wait_for)
534
+ body.wait_for = wait_for;
535
+ try {
536
+ const { ok, data } = await callAPI("/v1/scrape", body);
537
+ if (!ok) {
538
+ return errorResult(`Scrape failed: ${data.error || JSON.stringify(data)}`);
539
+ }
540
+ // Truncate long scrape content to avoid token explosion
541
+ const MAX_CONTENT_LENGTH = 15000;
542
+ const content = data.content;
543
+ if (content && content.length > MAX_CONTENT_LENGTH) {
544
+ data.content =
545
+ content.slice(0, MAX_CONTENT_LENGTH) +
546
+ `\n\n[... content truncated at ${MAX_CONTENT_LENGTH} chars. Total: ${content.length} chars. Use include_selectors to extract specific sections if you need more targeted content.]`;
547
+ }
548
+ return textResult(data);
467
549
  }
468
- return textResult(data);
469
- }
470
- catch (err) {
471
- return errorResult(`Failed to connect to AgentKey server at ${AGENTKEY_BASE_URL}: ${err}`);
472
- }
473
- });
474
- // Social media tool
475
- server.tool("agentkey_social", `Search and browse Twitter/X and Reddit content in real time. Returns actual social media data (posts, comments, profiles, trending topics) that web search cannot access directly.
550
+ catch (err) {
551
+ return errorResult(`Failed to connect to AgentKey server at ${AGENTKEY_BASE_URL}: ${err}`);
552
+ }
553
+ });
554
+ // Social media tool
555
+ server.tool("agentkey_social", `Search and browse social media content in real time across 11 platforms.
476
556
 
477
- USE THIS TOOL WHEN the user:
478
- - Mentions "Twitter", "X", "tweet", "推特", "推文", or any Twitter/X related query
479
- - Mentions "Reddit", "subreddit", "r/", or any Reddit related query
480
- - Asks about social media discussions, opinions, posts, or comments on a topic
481
- - Wants to look up a social media user's profile, posts, or followers
482
- - Asks for trending topics or what people are saying about something on social media
483
- - Asks for "最新讨论", "社区讨论", "网友评论", "大家怎么看" about any topic
557
+ Platforms: twitter, reddit, xiaohongshu (小红书), instagram, zhihu (知乎), tiktok, douyin (抖音), bilibili (B站), weibo (微博), threads, youtube.
484
558
 
485
- Platform detection:
486
- - "twitter" for tweets, X posts, 推特, 推文, Twitter users, trending
487
- - "reddit" for Reddit posts, subreddit content, Reddit discussions, r/ anything
559
+ THINK BEFORE CALLING — use your own knowledge first:
560
+ - For general knowledge about platforms, creators, or public figures, answer from your training data.
561
+ - Only call when the user needs REAL-TIME social media content: recent posts, live discussions, current trends, or specific post details.
562
+ - Examples that do NOT need this tool: "who is Elon Musk on Twitter", "what is Reddit", "how does TikTok algorithm work".
563
+ - Examples that NEED this tool: "what is @elonmusk tweeting about today", "trending topics on Weibo now", "latest posts in r/cryptocurrency".
488
564
 
489
- Type guide (default "search" if not specified):
490
- - search: Search posts/tweets by keyword. Requires "query".
491
- - posts: Get a user's tweets (Twitter) or subreddit posts (Reddit). Requires "username" or "subreddit".
492
- - comments/latest_comments: Get replies to a post. Requires "post_id".
493
- - user: Get user profile. Requires "username".
494
- - detail: Get a single post's full content. Requires "post_id".
495
- - replies/media/highlights: User's reply tweets, media tweets, or highlights. Requires "username".
496
- - followers/following: User's social graph. Requires "username".
497
- - retweets: Users who retweeted. Requires "post_id".
498
- - trending: Trending topics (Twitter only). Optional "country".
565
+ USE THIS TOOL (instead of agentkey_search) when the user asks about real-time social media content.
566
+
567
+ STRICT RULE ONE CALL AT A TIME. Pick ONE platform per call.
568
+
569
+ Platform guide:
570
+ - "twitter" tweets, trending. Use "username".
571
+ - "reddit" subreddit discussions. Use "subreddit".
572
+ - "xiaohongshu" 小红书 notes, products. Use "note_id"/"share_text".
573
+ - "instagram" posts, reels, stories, hashtags. Use "shortcode"/"media_id".
574
+ - "zhihu" 知乎 Q&A, articles, topics. Use "user_url_token"/"question_id"/"article_id".
575
+ - "tiktok" — TikTok videos, hashtags, music. Use "aweme_id"/"sec_user_id"/"unique_id".
576
+ - "douyin" — 抖音 videos, hot search, series. Use "aweme_id"/"sec_user_id".
577
+ - "bilibili" — B站 videos. Use "bv_id"/"av_id".
578
+ - "weibo" — 微博 posts, hot search. Use "uid"/"status_id".
579
+ - "threads" — Threads posts. Use "username"/"user_id".
580
+ - "youtube" — YouTube videos, channels, shorts. Use "video_id"/"channel_id".
581
+
582
+ Common types: search, posts, comments, user, detail, followers, following.
583
+ See full type list in the enum.`, {
584
+ platform: zod_1.z
585
+ .enum(["twitter", "reddit", "xiaohongshu", "instagram", "zhihu", "tiktok", "douyin", "bilibili", "weibo", "threads", "youtube"])
586
+ .describe("Social media platform"),
587
+ type: zod_1.z
588
+ .enum([
589
+ "search", "posts", "comments", "user", "detail", "followers", "following", "highlights",
590
+ "latest_comments", "replies", "media", "retweets", "trending",
591
+ "faved", "search_users", "search_images", "search_products", "search_groups",
592
+ "product_detail", "product_review_overview", "product_reviews", "product_recs",
593
+ "topic_info", "topic_feed", "creator_feed", "creator_hot_feed",
594
+ "search_hashtags", "general_search", "user_brief", "user_about", "tagged", "reels",
595
+ "stories", "highlight_stories", "comment_replies", "explore", "recommended_reels",
596
+ "location_info", "location_posts", "hashtag_posts",
597
+ "articles", "answers", "hot_list", "columns", "topics", "videos",
598
+ "hashtag_detail", "hashtag_videos", "music_detail", "music_videos", "liked", "home_feed",
599
+ "hot_search", "mix_detail", "mix_videos", "series_detail", "series_videos",
600
+ "popular", "search_by_type",
601
+ "ai_search", "timeline", "reposts", "likes", "album", "super_topics",
602
+ "search_top", "search_recent",
603
+ "channel_videos", "channel_shorts", "related", "captions", "search_shorts", "search_channels",
604
+ ])
605
+ .optional()
606
+ .describe("Query type. Default 'search'."),
607
+ query: zod_1.z.string().optional().describe("Search query."),
608
+ username: zod_1.z.string().optional().describe("Username without @ prefix."),
609
+ user_id: zod_1.z.string().optional().describe("User ID."),
610
+ post_id: zod_1.z.string().optional().describe("Post/tweet/note/video ID."),
611
+ note_id: zod_1.z.string().optional().describe("Xiaohongshu note ID."),
612
+ share_text: zod_1.z.string().optional().describe("Xiaohongshu sharing link."),
613
+ shortcode: zod_1.z.string().optional().describe("Instagram post shortcode."),
614
+ media_id: zod_1.z.string().optional().describe("Instagram media ID."),
615
+ comment_id: zod_1.z.string().optional().describe("Comment ID."),
616
+ hashtag: zod_1.z.string().optional().describe("Hashtag without #."),
617
+ location_id: zod_1.z.string().optional().describe("Instagram location ID."),
618
+ highlight_id: zod_1.z.string().optional().describe("Instagram highlight ID."),
619
+ sku_id: zod_1.z.string().optional().describe("Xiaohongshu product SKU ID."),
620
+ topic_id: zod_1.z.string().optional().describe("Xiaohongshu topic page ID."),
621
+ subreddit: zod_1.z.string().optional().describe("Subreddit name without r/."),
622
+ user_url_token: zod_1.z.string().optional().describe("Zhihu user URL token."),
623
+ question_id: zod_1.z.string().optional().describe("Zhihu question ID."),
624
+ article_id: zod_1.z.string().optional().describe("Zhihu article ID."),
625
+ column_id: zod_1.z.string().optional().describe("Zhihu column ID."),
626
+ answer_id: zod_1.z.string().optional().describe("Zhihu answer ID."),
627
+ sec_user_id: zod_1.z.string().optional().describe("TikTok/Douyin sec_user_id."),
628
+ unique_id: zod_1.z.string().optional().describe("TikTok username (unique_id)."),
629
+ aweme_id: zod_1.z.string().optional().describe("TikTok/Douyin video ID."),
630
+ music_id: zod_1.z.string().optional().describe("Music/audio ID."),
631
+ ch_id: zod_1.z.string().optional().describe("Hashtag/challenge ID."),
632
+ share_url: zod_1.z.string().optional().describe("Share URL."),
633
+ mix_id: zod_1.z.string().optional().describe("Douyin mix/collection ID."),
634
+ series_id: zod_1.z.string().optional().describe("Douyin series ID."),
635
+ av_id: zod_1.z.string().optional().describe("Bilibili AV ID."),
636
+ bv_id: zod_1.z.string().optional().describe("Bilibili BV ID."),
637
+ search_type: zod_1.z.string().optional().describe("Bilibili search type (video/bangumi/live/article/user)."),
638
+ uid: zod_1.z.string().optional().describe("Weibo user ID."),
639
+ status_id: zod_1.z.string().optional().describe("Weibo status/post ID."),
640
+ category: zod_1.z.string().optional().describe("Category filter."),
641
+ video_id: zod_1.z.string().optional().describe("YouTube video ID."),
642
+ channel_id: zod_1.z.string().optional().describe("YouTube channel ID."),
643
+ channel_url: zod_1.z.string().optional().describe("YouTube channel URL."),
644
+ sort: zod_1.z.enum(["relevance", "hot", "top", "new", "latest", "comments"]).optional().describe("Sort order."),
645
+ time: zod_1.z.enum(["hour", "day", "week", "month", "year", "all"]).optional().describe("Time filter."),
646
+ num: zod_1.z.coerce.number().min(1).max(100).optional().describe("Number of results, default 10."),
647
+ page: zod_1.z.coerce.number().min(1).optional().describe("Page number."),
648
+ country: zod_1.z.string().optional().describe("Country code."),
649
+ cursor: zod_1.z.string().optional().describe("Pagination cursor."),
650
+ }, async (params) => {
651
+ const body = {};
652
+ for (const [k, v] of Object.entries(params)) {
653
+ if (v !== undefined && v !== null)
654
+ body[k] = v;
655
+ }
656
+ try {
657
+ const { ok, data } = await callAPI("/v1/social", body);
658
+ if (!ok) {
659
+ return errorResult(`Social query failed: ${data.error || JSON.stringify(data)}`);
660
+ }
661
+ const results = data.results;
662
+ const count = results?.length ?? 0;
663
+ const hint = count > 0
664
+ ? `\n\n[${count} results returned. Analyze these and answer the user. Only make a follow-up call if the user explicitly asks for more details.]`
665
+ : `\n\n[No results. Try broader keywords or a different type/sort. Do NOT retry the same query.]`;
666
+ const compact = compactResult(data);
667
+ compact.content[0].text += hint;
668
+ return compact;
669
+ }
670
+ catch (err) {
671
+ return errorResult(`Failed to connect to AgentKey server at ${AGENTKEY_BASE_URL}: ${err}`);
672
+ }
673
+ });
674
+ // Crypto tool
675
+ server.tool("agentkey_crypto", `Query blockchain and crypto data in real time. Three data sources in one tool:
676
+ (1) Chainbase on-chain data — token holders, wallet balances, NFTs, transactions, ENS domains, DeFi portfolios, smart contract calls, SQL on-chain queries.
677
+ (2) CoinMarketCap market data — crypto rankings, price quotes, market cap, trending coins, gainers/losers, categories, global metrics.
678
+ (3) Crypto social intelligence (Tops) — trending crypto narratives, social mentions, Twitter/X crypto discussions, topic discovery.
679
+
680
+ THINK BEFORE CALLING — use your own knowledge first:
681
+ - For crypto concepts, protocol explanations, or historical facts, answer from your training data WITHOUT calling this tool.
682
+ - Only call when the user needs REAL-TIME data: live prices, current rankings, on-chain balances, recent transactions, or today's trends.
683
+ - Examples that do NOT need this tool: "what is DeFi", "explain impermanent loss", "how does Uniswap work", "what is proof of stake".
684
+ - Examples that NEED this tool: "BTC price now", "top gainers today", "check wallet balance at 0x...", "trending crypto narratives this week".
685
+
686
+ Supported chains (Chainbase): Ethereum (1), BSC (56), Polygon (137), Avalanche (43114), Arbitrum (42161), Optimism (10), Base (8453), zkSync (324).
687
+
688
+ Common token contracts (Ethereum):
689
+ - USDT: 0xdAC17F958D2ee523a2206206994597C13D831ec7
690
+ - USDC: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
691
+ - WETH: 0xC02aaA39b223FE8D0A0e5c4F27eAD9083C756Cc2
692
+ - DAI: 0x6B175474E89094C44Da98b954EedeAC495271d0F
693
+ - WBTC: 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599
694
+
695
+ Routing — match user intent to the right type:
696
+
697
+ CoinMarketCap market data (use symbol like "BTC", "ETH" — no contract address needed):
698
+ - "top coins" / "crypto rankings" / "币圈排行" → cmc_listings (sort by market_cap)
699
+ - "BTC price" / "ETH price" / "coin price by name" → cmc_quotes (needs symbol like "BTC,ETH")
700
+ - "what is Bitcoin" / "coin info" / "coin logo" → cmc_info (needs symbol)
701
+ - "total market cap" / "BTC dominance" / "市场总览" → cmc_global_metrics
702
+ - "trending coins" / "热门币种" → cmc_trending
703
+ - "biggest gainers" / "biggest losers" / "涨幅榜" / "跌幅榜" → cmc_gainers_losers (sort_dir: "desc" for gainers, "asc" for losers)
704
+ - "most popular coins" / "most visited" → cmc_most_visited
705
+ - "DeFi category" / "meme coins" / "crypto categories" → cmc_categories or cmc_category (needs category_id)
706
+ - "convert 1 BTC to USD" / "币价换算" → cmc_price_conversion (needs amount, symbol, convert)
707
+ - "find coin by symbol" / "coin ID lookup" → cmc_map (needs symbol)
708
+
709
+ Chainbase on-chain data (use contract_address or wallet address):
710
+ - "on-chain token price" / "token price by contract" → token_price (needs contract_address)
711
+ - "token metadata by contract" → token_metadata (needs contract_address)
712
+ - "price history" / "历史价格" → token_price_history (needs contract_address, from_timestamp, end_timestamp)
713
+ - "who holds" / "top holders" / "持币大户" → token_top_holders (needs contract_address)
714
+ - "token transfers" / "转账记录" → token_transfers (needs contract_address)
715
+ - "wallet balance" / "ETH balance" / "余额" → balance (needs address)
716
+ - "ERC20 holdings" / "持仓" → tokens (needs address)
717
+ - "NFT holdings" → nfts (needs address)
718
+ - "DeFi portfolio" → portfolios (needs address)
719
+ - "latest block" → block_latest
720
+ - "block detail" → block_detail (needs block_number)
721
+ - "transaction detail" → tx_detail (needs hash)
722
+ - "transaction history" → tx_list (needs address)
723
+ - "NFT floor price" → nft_floor_price (needs contract_address)
724
+ - "NFT collection" → nft_collection (needs contract_address)
725
+ - "trending NFT" → nft_collection_trending
726
+ - "ENS → address" → ens_resolve (needs domain)
727
+ - "address → ENS" → ens_reverse (needs address)
728
+ - "address label" → address_labels (needs address)
729
+ - "call contract function" → contract_call (needs contract_address, function_name, abi)
730
+ - "SQL query" → sql_execute (needs sql)
731
+
732
+ Crypto social intelligence (Tops):
733
+ - "what's trending in crypto" / "加密热点" → trending
734
+ - "topic detail" → topic (needs topic_id)
735
+ - "topic posts/tweets" → topic_posts (needs topic_id)
736
+ - "crypto mentions" → mentions (needs query)
737
+ - "narrative search" → tops_search (needs query)
738
+
739
+ Decision guide — CoinMarketCap vs Chainbase:
740
+ - Use cmc_* types when user asks by coin NAME/SYMBOL (BTC, ETH) without a specific contract address
741
+ - Use Chainbase types when user provides a contract address (0x...) or needs on-chain data (holders, transfers, balances, NFTs)
742
+ - For general "what's the price of Bitcoin" → cmc_quotes; for "price of token 0xA0b8..." → token_price
743
+
744
+ SQL fallback: Use sql_execute for complex queries. Common tables: {chain}.blocks, {chain}.transactions, {chain}.token_transfers, {chain}.token_metas, {chain}.logs
499
745
 
500
746
  Preferences:
501
- - Strip @ from Twitter usernames (e.g. @elonmusk → "elonmusk"), strip r/ from subreddits (e.g. r/LocalLLaMA → "LocalLLaMA").
502
- - Use "sort": "latest" or "new" when user asks for latest/newest/最新 content.
503
- - Use "time": "week" or "day" when user asks for recent discussions.`, {
504
- platform: zod_1.z
505
- .enum(["twitter", "reddit"])
506
- .describe("Social media platform"),
507
- type: zod_1.z
508
- .enum([
509
- "search",
510
- "posts",
511
- "comments",
512
- "user",
513
- "detail",
514
- "latest_comments",
515
- "replies",
516
- "media",
517
- "highlights",
518
- "followers",
519
- "following",
520
- "retweets",
521
- "trending",
522
- ])
523
- .optional()
524
- .describe("Query type. Default 'search'. See tool description for platform-specific types and required parameters."),
525
- query: zod_1.z
526
- .string()
527
- .optional()
528
- .describe("Search query (required for type=search)"),
529
- username: zod_1.z
530
- .string()
531
- .optional()
532
- .describe("Username without @ (for type=posts/user/replies/media/highlights/followers/following)"),
533
- user_id: zod_1.z
534
- .string()
535
- .optional()
536
- .describe("Twitter user ID (for type=highlights, alternative to username)"),
537
- post_id: zod_1.z
538
- .string()
539
- .optional()
540
- .describe("Post/tweet ID (for type=detail/comments/latest_comments/retweets)"),
541
- subreddit: zod_1.z
542
- .string()
543
- .optional()
544
- .describe("Subreddit name without r/ (for Reddit type=posts)"),
545
- sort: zod_1.z
546
- .enum(["relevance", "hot", "top", "new", "latest", "comments"])
547
- .optional()
548
- .describe("Sort order. Default 'relevance' for search, 'hot' for posts."),
549
- time: zod_1.z
550
- .enum(["hour", "day", "week", "month", "year", "all"])
551
- .optional()
552
- .describe("Time filter for sort=top results"),
553
- num: zod_1.z
554
- .coerce.number()
555
- .min(1)
556
- .max(100)
557
- .optional()
558
- .describe("Number of results, default 10"),
559
- country: zod_1.z
560
- .string()
561
- .optional()
562
- .describe("Country for trending topics (Twitter only). Default 'UnitedStates'."),
563
- cursor: zod_1.z
564
- .string()
565
- .optional()
566
- .describe("Pagination cursor from previous response's next_cursor"),
567
- }, async ({ platform, type, query, username, user_id, post_id, subreddit, sort, time, num, country, cursor, }) => {
568
- const body = { platform };
569
- if (type)
570
- body.type = type;
571
- if (query)
572
- body.query = query;
573
- if (username)
574
- body.username = username;
575
- if (user_id)
576
- body.user_id = user_id;
577
- if (post_id)
578
- body.post_id = post_id;
579
- if (subreddit)
580
- body.subreddit = subreddit;
581
- if (sort)
582
- body.sort = sort;
583
- if (time)
584
- body.time = time;
585
- if (num)
586
- body.num = num;
587
- if (country)
588
- body.country = country;
589
- if (cursor)
590
- body.cursor = cursor;
591
- try {
592
- const { ok, data } = await callAPI("/v1/social", body);
593
- if (!ok) {
594
- return errorResult(`Social query failed: ${data.error || JSON.stringify(data)}`);
747
+ - Default chain_id to 1 (Ethereum) for Chainbase types.
748
+ - If user says a token name without contract address, prefer cmc_quotes (by symbol) over token_price (by contract).
749
+ - Default page=1, limit=20 for paginated results.
750
+ - Default convert to "USD" for CoinMarketCap types.`, {
751
+ type: zod_1.z
752
+ .enum([
753
+ "token_price",
754
+ "token_metadata",
755
+ "token_price_history",
756
+ "token_holders",
757
+ "token_top_holders",
758
+ "token_transfers",
759
+ "nft_metadata",
760
+ "nft_collection",
761
+ "nft_collection_items",
762
+ "nft_search",
763
+ "nft_transfers",
764
+ "nft_owner",
765
+ "nft_owners",
766
+ "nft_floor_price",
767
+ "nft_price_history",
768
+ "nft_collection_trending",
769
+ "balance",
770
+ "tokens",
771
+ "nfts",
772
+ "portfolios",
773
+ "block_latest",
774
+ "block_detail",
775
+ "tx_detail",
776
+ "tx_list",
777
+ "ens",
778
+ "ens_resolve",
779
+ "ens_reverse",
780
+ "spaceid_resolve",
781
+ "spaceid_reverse",
782
+ "address_labels",
783
+ "contract_call",
784
+ "sql_execute",
785
+ "trending",
786
+ "topic",
787
+ "topic_posts",
788
+ "mentions",
789
+ "tops_search",
790
+ "cmc_listings",
791
+ "cmc_quotes",
792
+ "cmc_info",
793
+ "cmc_map",
794
+ "cmc_global_metrics",
795
+ "cmc_trending",
796
+ "cmc_gainers_losers",
797
+ "cmc_most_visited",
798
+ "cmc_categories",
799
+ "cmc_category",
800
+ "cmc_price_conversion",
801
+ ])
802
+ .describe("Query type — see tool description for routing guide"),
803
+ chain_id: zod_1.z
804
+ .coerce.number()
805
+ .optional()
806
+ .describe("Blockchain chain ID. Default 1 (Ethereum). Common: BSC=56, Polygon=137, Arbitrum=42161, Base=8453, Optimism=10, Avalanche=43114, zkSync=324."),
807
+ contract_address: zod_1.z
808
+ .string()
809
+ .optional()
810
+ .describe("Token or NFT contract address (0x...)"),
811
+ address: zod_1.z
812
+ .string()
813
+ .optional()
814
+ .describe("Wallet address (0x...) for balance, tx, ENS reverse, etc."),
815
+ token_id: zod_1.z
816
+ .string()
817
+ .optional()
818
+ .describe("NFT token ID within a collection"),
819
+ hash: zod_1.z.string().optional().describe("Transaction hash for tx_detail"),
820
+ block_number: zod_1.z
821
+ .coerce.number()
822
+ .optional()
823
+ .describe("Block number for block_detail"),
824
+ domain: zod_1.z
825
+ .string()
826
+ .optional()
827
+ .describe("ENS or Space ID domain name (e.g. 'vitalik.eth')"),
828
+ query: zod_1.z
829
+ .string()
830
+ .optional()
831
+ .describe("Search keyword for nft_search, mentions, or tops_search"),
832
+ name: zod_1.z.string().optional().describe("NFT collection name for nft_search"),
833
+ topic_id: zod_1.z
834
+ .string()
835
+ .optional()
836
+ .describe("Topic ID for topic/topic_posts (from trending results)"),
837
+ sql: zod_1.z
838
+ .string()
839
+ .optional()
840
+ .describe("SQL query for sql_execute. Example: SELECT * FROM ethereum.blocks ORDER BY number DESC LIMIT 5"),
841
+ function_name: zod_1.z
842
+ .string()
843
+ .optional()
844
+ .describe("Smart contract function name for contract_call (e.g. 'balanceOf')"),
845
+ abi: zod_1.z
846
+ .string()
847
+ .optional()
848
+ .describe("Contract ABI as JSON string for contract_call"),
849
+ params: zod_1.z
850
+ .array(zod_1.z.string())
851
+ .optional()
852
+ .describe("Function parameters for contract_call (array of strings)"),
853
+ from_block: zod_1.z.coerce.number().optional().describe("Start block number"),
854
+ to_block: zod_1.z.coerce.number().optional().describe("End block number"),
855
+ from_timestamp: zod_1.z
856
+ .coerce.number()
857
+ .optional()
858
+ .describe("Start timestamp (unix seconds)"),
859
+ end_timestamp: zod_1.z
860
+ .coerce.number()
861
+ .optional()
862
+ .describe("End timestamp (unix seconds)"),
863
+ sort: zod_1.z
864
+ .string()
865
+ .optional()
866
+ .describe("Sort order for applicable queries"),
867
+ page: zod_1.z
868
+ .coerce.number()
869
+ .min(1)
870
+ .optional()
871
+ .describe("Page number, default 1"),
872
+ limit: zod_1.z
873
+ .coerce.number()
874
+ .min(1)
875
+ .max(100)
876
+ .optional()
877
+ .describe("Results per page, default 20"),
878
+ language: zod_1.z
879
+ .string()
880
+ .optional()
881
+ .describe("Language for trending topics (e.g. 'en', 'zh')"),
882
+ // CoinMarketCap params
883
+ symbol: zod_1.z
884
+ .string()
885
+ .optional()
886
+ .describe("Comma-separated coin symbols for cmc_* types (e.g. 'BTC,ETH'). Use this instead of contract_address for CoinMarketCap queries."),
887
+ slug: zod_1.z
888
+ .string()
889
+ .optional()
890
+ .describe("Comma-separated coin slugs (e.g. 'bitcoin,ethereum')"),
891
+ cmc_id: zod_1.z
892
+ .string()
893
+ .optional()
894
+ .describe("Comma-separated CoinMarketCap IDs"),
895
+ convert: zod_1.z
896
+ .string()
897
+ .optional()
898
+ .describe("Conversion currency for cmc_* types (e.g. 'USD', 'EUR', 'BTC'). Default 'USD'."),
899
+ category_id: zod_1.z
900
+ .string()
901
+ .optional()
902
+ .describe("CoinMarketCap category ID for cmc_category"),
903
+ time_period: zod_1.z
904
+ .enum(["1h", "24h", "7d", "30d"])
905
+ .optional()
906
+ .describe("Time period for cmc_trending/cmc_gainers_losers/cmc_most_visited. Default '24h'."),
907
+ sort_dir: zod_1.z
908
+ .enum(["asc", "desc"])
909
+ .optional()
910
+ .describe("Sort direction. For cmc_gainers_losers: 'desc'=gainers first, 'asc'=losers first."),
911
+ amount: zod_1.z
912
+ .coerce.number()
913
+ .optional()
914
+ .describe("Amount for cmc_price_conversion (e.g. 1.5)"),
915
+ cryptocurrency_type: zod_1.z
916
+ .enum(["all", "coins", "tokens"])
917
+ .optional()
918
+ .describe("Filter for cmc_listings: 'all', 'coins', or 'tokens'"),
919
+ tag: zod_1.z
920
+ .string()
921
+ .optional()
922
+ .describe("Filter tag for cmc_listings (e.g. 'defi', 'filesharing')"),
923
+ price_min: zod_1.z.coerce.number().optional().describe("Min USD price filter for cmc_listings"),
924
+ price_max: zod_1.z.coerce.number().optional().describe("Max USD price filter for cmc_listings"),
925
+ market_cap_min: zod_1.z.coerce.number().optional().describe("Min market cap filter for cmc_listings"),
926
+ market_cap_max: zod_1.z.coerce.number().optional().describe("Max market cap filter for cmc_listings"),
927
+ volume_24h_min: zod_1.z.coerce.number().optional().describe("Min 24h volume filter for cmc_listings"),
928
+ volume_24h_max: zod_1.z.coerce.number().optional().describe("Max 24h volume filter for cmc_listings"),
929
+ }, async ({ type, chain_id, contract_address, address, token_id, hash, block_number, domain, query, name, topic_id, sql, function_name, abi, params, from_block, to_block, from_timestamp, end_timestamp, sort, page, limit, language, symbol, slug, cmc_id, convert, category_id, time_period, sort_dir, amount, cryptocurrency_type, tag, price_min, price_max, market_cap_min, market_cap_max, volume_24h_min, volume_24h_max, }) => {
930
+ const body = { type };
931
+ if (chain_id)
932
+ body.chain_id = chain_id;
933
+ if (contract_address)
934
+ body.contract_address = contract_address;
935
+ if (address)
936
+ body.address = address;
937
+ if (token_id)
938
+ body.token_id = token_id;
939
+ if (hash)
940
+ body.hash = hash;
941
+ if (block_number)
942
+ body.block_number = block_number;
943
+ if (domain)
944
+ body.domain = domain;
945
+ if (query)
946
+ body.query = query;
947
+ if (name)
948
+ body.name = name;
949
+ if (topic_id)
950
+ body.topic_id = topic_id;
951
+ if (sql)
952
+ body.sql = sql;
953
+ if (function_name)
954
+ body.function_name = function_name;
955
+ if (abi)
956
+ body.abi = abi;
957
+ if (params?.length)
958
+ body.params = params;
959
+ if (from_block)
960
+ body.from_block = from_block;
961
+ if (to_block)
962
+ body.to_block = to_block;
963
+ if (from_timestamp)
964
+ body.from_timestamp = from_timestamp;
965
+ if (end_timestamp)
966
+ body.end_timestamp = end_timestamp;
967
+ if (sort)
968
+ body.sort = sort;
969
+ if (page)
970
+ body.page = page;
971
+ if (limit)
972
+ body.limit = limit;
973
+ if (language)
974
+ body.language = language;
975
+ if (symbol)
976
+ body.symbol = symbol;
977
+ if (slug)
978
+ body.slug = slug;
979
+ if (cmc_id)
980
+ body.cmc_id = cmc_id;
981
+ if (convert)
982
+ body.convert = convert;
983
+ if (category_id)
984
+ body.category_id = category_id;
985
+ if (time_period)
986
+ body.time_period = time_period;
987
+ if (sort_dir)
988
+ body.sort_dir = sort_dir;
989
+ if (amount)
990
+ body.amount = amount;
991
+ if (cryptocurrency_type)
992
+ body.cryptocurrency_type = cryptocurrency_type;
993
+ if (tag)
994
+ body.tag = tag;
995
+ if (price_min)
996
+ body.price_min = price_min;
997
+ if (price_max)
998
+ body.price_max = price_max;
999
+ if (market_cap_min)
1000
+ body.market_cap_min = market_cap_min;
1001
+ if (market_cap_max)
1002
+ body.market_cap_max = market_cap_max;
1003
+ if (volume_24h_min)
1004
+ body.volume_24h_min = volume_24h_min;
1005
+ if (volume_24h_max)
1006
+ body.volume_24h_max = volume_24h_max;
1007
+ try {
1008
+ const { ok, data } = await callAPI("/v1/crypto", body);
1009
+ if (!ok) {
1010
+ return errorResult(`Crypto query failed: ${data.error || JSON.stringify(data)}`);
1011
+ }
1012
+ const compact = compactResult(data);
1013
+ compact.content[0].text +=
1014
+ `\n\n[Data returned. Present the results to the user. Do NOT make follow-up crypto calls unless the user asks for additional data.]`;
1015
+ return compact;
595
1016
  }
596
- return textResult(data);
597
- }
598
- catch (err) {
599
- return errorResult(`Failed to connect to AgentKey server at ${AGENTKEY_BASE_URL}: ${err}`);
600
- }
601
- });
1017
+ catch (err) {
1018
+ return errorResult(`Failed to connect to AgentKey server at ${AGENTKEY_BASE_URL}: ${err}`);
1019
+ }
1020
+ });
1021
+ } // end registerBuiltinTools()
602
1022
  // --- Start ---
1023
+ const POLL_INTERVAL_MS = parseInt(process.env.AGENTKEY_POLL_INTERVAL || "30000", 10);
603
1024
  async function main() {
604
- const transport = new stdio_js_1.StdioServerTransport();
605
- await server.connect(transport);
606
- console.error("AgentKey MCP Server running on stdio");
1025
+ let dynamicManager = null;
1026
+ // Try dynamic tool loading from /v1/tools first
1027
+ const registry = await (0, dynamic_js_1.fetchToolRegistry)(AGENTKEY_BASE_URL);
1028
+ if (registry && registry.tools.length > 0) {
1029
+ dynamicManager = new dynamic_js_1.DynamicToolManager(server, callAPI, AGENTKEY_BASE_URL, POLL_INTERVAL_MS);
1030
+ const count = dynamicManager.registerAll(registry);
1031
+ dynamicManager.startPolling();
1032
+ console.error(`AgentKey MCP Server running on stdio (dynamic: ${count} tools, hot-reload every ${POLL_INTERVAL_MS / 1000}s)`);
1033
+ }
1034
+ else {
1035
+ // REST API server not reachable — fall back to hardcoded tool definitions.
1036
+ registerBuiltinTools();
1037
+ console.error("AgentKey MCP Server running on stdio (using built-in tool definitions)");
1038
+ }
607
1039
  console.error(` API: ${AGENTKEY_BASE_URL}`);
608
1040
  console.error(` Key: ${AGENTKEY_API_KEY ? "****" + AGENTKEY_API_KEY.slice(-4) : "(not set)"}`);
1041
+ const transport = new stdio_js_1.StdioServerTransport();
1042
+ await server.connect(transport);
1043
+ // Cleanup on shutdown
1044
+ const cleanup = () => {
1045
+ dynamicManager?.stopPolling();
1046
+ };
1047
+ process.on("beforeExit", cleanup);
609
1048
  }
610
1049
  // Graceful shutdown
611
1050
  process.on("SIGINT", () => {