@agentkey/mcp 0.0.4 → 0.0.5

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.5
50
51
 
51
52
  Tools:
52
53
  agentkey_search Search the web, news, images, videos, or places
@@ -55,6 +56,8 @@ Tools:
55
56
  Providers: Firecrawl, Jina Reader, ScrapeNinja
56
57
  agentkey_social Query social media (search, posts, comments, user profiles)
57
58
  Platforms: Twitter/X, Reddit
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,189 +295,247 @@ 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.5",
314
+ description: `AgentKey provides 4 tools: search, scrape, social, crypto. Follow this decision tree:
315
+
316
+ 1. Is the user asking about crypto prices, on-chain data, wallets, or NFTs? → Use agentkey_crypto. Do NOT use agentkey_search for crypto prices.
317
+ 2. Is the user asking about Twitter/X or Reddit content? → Use agentkey_social. Do NOT use agentkey_search for social media.
318
+ 3. Does the user want the full content of a specific URL? → Use agentkey_scrape.
319
+ 4. Everything else (web search, news, general questions) → Use agentkey_search.
320
+
321
+ GLOBAL RULES:
322
+ - Call at most ONE tool per turn. Never batch multiple tool calls in parallel.
323
+ - Analyze results before deciding if another call is needed.
324
+ - If a tool returns useful results, answer the user — do not call another tool "for more context".
325
+ - 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
326
  });
300
- // Search tool
301
- server.tool("agentkey_search", `Search the web, news, images, videos, or places via AgentKey unified API.
327
+ // --- Built-in (static) tool definitions ---
328
+ // Only used as fallback when the REST API server is unreachable for dynamic loading.
329
+ function registerBuiltinTools() {
330
+ // Search tool
331
+ server.tool("agentkey_search", `Search the web, news, images, videos, or places via AgentKey unified API.
302
332
  Aggregates results from Serper, Tavily, Brave, and Perplexity with automatic failover.
303
333
 
304
334
  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.
305
335
 
336
+ STRICT RULE — MAXIMUM ONE SEARCH CALL AT A TIME:
337
+ - You MUST call this tool at most ONCE per turn. Wait for results, analyze them, and only then decide if another search is needed.
338
+ - NEVER batch multiple search calls in parallel. If you find yourself about to make 2+ search calls at once, STOP — pick the single best query and make only that one call.
339
+ - After receiving results, answer the user if possible. Only make a follow-up search if the results are clearly insufficient AND you have a meaningfully different query to try.
340
+
341
+ QUERY OPTIMIZATION:
342
+ - 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.
343
+ - Do NOT search the same topic from different angles (e.g. "X review" + "X opinion" + "X comparison"). Use one comprehensive query.
344
+ - Do NOT search the same topic in multiple languages. Pick the best language for that topic and search once.
345
+ - If you already have a URL from search results and need details, use agentkey_scrape instead of searching again.
346
+ - Default num to 5. Use 10 only when the user explicitly needs comprehensive results.
347
+
306
348
  Preferences:
307
349
  - Default provider to "auto" for best speed and reliability. Use a specific provider only when the user explicitly requests it.
308
350
  - 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.
310
351
  - Default type to "web" unless the user asks for news, images, videos, or places.
311
352
  - Use time_range when the user asks for "recent", "latest", "this week", "today" etc.
312
353
  - 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)}`);
354
+ query: zod_1.z.string().describe("Search query"),
355
+ provider: zod_1.z
356
+ .enum(["auto", "serper", "tavily", "brave", "perplexity"])
357
+ .optional()
358
+ .describe("Search provider. Default 'auto' (fastest & cheapest). Only specify when user explicitly asks."),
359
+ type: zod_1.z
360
+ .enum(["web", "news", "images", "videos", "places"])
361
+ .optional()
362
+ .describe("Search type. Default 'web'. Use 'news' for recent articles, 'images' for pictures, 'videos' for video content, 'places' for locations/businesses."),
363
+ search_depth: zod_1.z
364
+ .enum(["basic", "advanced"])
365
+ .optional()
366
+ .describe("Search depth. 'basic' for fast results, 'advanced' for more thorough and detailed results. Default 'basic'."),
367
+ num: zod_1.z
368
+ .coerce.number()
369
+ .min(1)
370
+ .max(50)
371
+ .optional()
372
+ .describe("Number of results, default 5. Use 10 only when user needs comprehensive coverage."),
373
+ country: zod_1.z
374
+ .string()
375
+ .optional()
376
+ .describe("Country code (ISO 3166-1 alpha-2) to localize results, e.g. 'us', 'cn', 'jp'"),
377
+ lang: zod_1.z
378
+ .string()
379
+ .optional()
380
+ .describe("Language code for results. Auto-detect from user's query language: Chinese→'zh', English→'en', Japanese→'ja', etc. User can override."),
381
+ time_range: zod_1.z
382
+ .enum(["day", "week", "month", "year"])
383
+ .optional()
384
+ .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."),
385
+ include_domains: zod_1.z
386
+ .array(zod_1.z.string())
387
+ .optional()
388
+ .describe("Only include results from these domains, e.g. ['github.com', 'arxiv.org']"),
389
+ exclude_domains: zod_1.z
390
+ .array(zod_1.z.string())
391
+ .optional()
392
+ .describe("Exclude results from these domains, e.g. ['pinterest.com', 'quora.com']"),
393
+ page: zod_1.z
394
+ .coerce.number()
395
+ .min(1)
396
+ .optional()
397
+ .describe("Page number for pagination, starting from 1"),
398
+ }, async ({ query, provider, type, search_depth, num, country, lang, time_range, include_domains, exclude_domains, page, }) => {
399
+ const body = {
400
+ query,
401
+ provider: provider || "auto",
402
+ };
403
+ if (type)
404
+ body.type = type;
405
+ if (search_depth)
406
+ body.search_depth = search_depth;
407
+ if (num)
408
+ body.num = num;
409
+ if (country)
410
+ body.country = country;
411
+ if (lang)
412
+ body.lang = lang;
413
+ if (time_range)
414
+ body.time_range = time_range;
415
+ if (include_domains?.length)
416
+ body.include_domains = include_domains;
417
+ if (exclude_domains?.length)
418
+ body.exclude_domains = exclude_domains;
419
+ if (page)
420
+ body.page = page;
421
+ try {
422
+ const { ok, data } = await callAPI("/v1/search", body);
423
+ if (!ok) {
424
+ return errorResult(`Search failed: ${data.error || JSON.stringify(data)}`);
425
+ }
426
+ const results = data.results;
427
+ const count = results?.length ?? 0;
428
+ const hint = count > 0
429
+ ? `\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.]`
430
+ : `\n\n[No results. Try broader keywords or a different type/time_range. Do NOT retry the same query.]`;
431
+ const compact = compactResult(data);
432
+ compact.content[0].text += hint;
433
+ return compact;
384
434
  }
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.
435
+ catch (err) {
436
+ return errorResult(`Failed to connect to AgentKey server at ${AGENTKEY_BASE_URL}: ${err}`);
437
+ }
438
+ });
439
+ // Scrape tool
440
+ server.tool("agentkey_scrape", `Scrape a webpage and extract its content as markdown, HTML, text, or screenshot via AgentKey unified API.
393
441
  Aggregates Firecrawl, Jina Reader, and ScrapeNinja with automatic failover.
394
442
 
443
+ STRICT RULE — MAXIMUM ONE SCRAPE CALL AT A TIME:
444
+ - You MUST call this tool at most ONCE per turn. Do NOT batch-scrape multiple URLs in parallel.
445
+ - Only scrape when you actually need the full page content. If search results already gave you enough info (title, snippet, date), do NOT scrape.
446
+ - Scrape only the single most relevant URL. NEVER scrape 2+ pages "to be thorough".
447
+
395
448
  Preferences:
396
449
  - Default provider to "auto" (uses Jina Reader — fastest and cheapest).
397
450
  - Default format to "markdown" which is best for LLM consumption.
398
451
  - Use "include_links": true when the user wants to know what links are on the page.
399
452
  - Use CSS selectors (include_selectors/exclude_selectors) when the user wants specific parts of the page.
400
453
  - 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)}`);
454
+ url: zod_1.z
455
+ .string()
456
+ .describe("URL to scrape (must start with http:// or https://)"),
457
+ provider: zod_1.z
458
+ .enum(["auto", "firecrawl", "jina", "scrapeninja"])
459
+ .optional()
460
+ .describe("Scrape provider. Default 'auto' (Jina Reader). Use 'firecrawl' for JS-heavy pages or advanced extraction."),
461
+ format: zod_1.z
462
+ .enum(["markdown", "html", "text", "screenshot"])
463
+ .optional()
464
+ .describe("Output format. Default 'markdown'. Best for LLM consumption."),
465
+ include_selectors: zod_1.z
466
+ .array(zod_1.z.string())
467
+ .optional()
468
+ .describe("CSS selectors to include, e.g. ['.article', '#main-content']"),
469
+ exclude_selectors: zod_1.z
470
+ .array(zod_1.z.string())
471
+ .optional()
472
+ .describe("CSS selectors to exclude, e.g. ['.nav', '.footer', '.sidebar']"),
473
+ include_links: zod_1.z
474
+ .boolean()
475
+ .optional()
476
+ .describe("Include extracted links from the page"),
477
+ include_images: zod_1.z
478
+ .boolean()
479
+ .optional()
480
+ .describe("Include extracted image URLs from the page"),
481
+ timeout: zod_1.z
482
+ .coerce.number()
483
+ .min(1)
484
+ .max(120)
485
+ .optional()
486
+ .describe("Timeout in seconds, default 30"),
487
+ country: zod_1.z
488
+ .string()
489
+ .optional()
490
+ .describe("Proxy country code for geo-restricted content, e.g. 'us', 'cn'"),
491
+ wait_for: zod_1.z
492
+ .coerce.number()
493
+ .optional()
494
+ .describe("Wait milliseconds before extracting content. Useful for pages with delayed JS rendering."),
495
+ }, async ({ url, provider, format, include_selectors, exclude_selectors, include_links, include_images, timeout, country, wait_for, }) => {
496
+ const body = {
497
+ url,
498
+ provider: provider || "auto",
499
+ };
500
+ if (format)
501
+ body.format = format;
502
+ if (include_selectors?.length)
503
+ body.include_selectors = include_selectors;
504
+ if (exclude_selectors?.length)
505
+ body.exclude_selectors = exclude_selectors;
506
+ if (include_links)
507
+ body.include_links = include_links;
508
+ if (include_images)
509
+ body.include_images = include_images;
510
+ if (timeout)
511
+ body.timeout = timeout;
512
+ if (country)
513
+ body.country = country;
514
+ if (wait_for)
515
+ body.wait_for = wait_for;
516
+ try {
517
+ const { ok, data } = await callAPI("/v1/scrape", body);
518
+ if (!ok) {
519
+ return errorResult(`Scrape failed: ${data.error || JSON.stringify(data)}`);
520
+ }
521
+ // Truncate long scrape content to avoid token explosion
522
+ const MAX_CONTENT_LENGTH = 15000;
523
+ const content = data.content;
524
+ if (content && content.length > MAX_CONTENT_LENGTH) {
525
+ data.content =
526
+ content.slice(0, MAX_CONTENT_LENGTH) +
527
+ `\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.]`;
528
+ }
529
+ return textResult(data);
467
530
  }
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.
531
+ catch (err) {
532
+ return errorResult(`Failed to connect to AgentKey server at ${AGENTKEY_BASE_URL}: ${err}`);
533
+ }
534
+ });
535
+ // Social media tool
536
+ 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.
476
537
 
477
- USE THIS TOOL WHEN the user:
538
+ USE THIS TOOL (instead of agentkey_search) WHEN the user:
478
539
  - Mentions "Twitter", "X", "tweet", "推特", "推文", or any Twitter/X related query
479
540
  - Mentions "Reddit", "subreddit", "r/", or any Reddit related query
480
541
  - Asks about social media discussions, opinions, posts, or comments on a topic
@@ -482,6 +543,13 @@ USE THIS TOOL WHEN the user:
482
543
  - Asks for trending topics or what people are saying about something on social media
483
544
  - Asks for "最新讨论", "社区讨论", "网友评论", "大家怎么看" about any topic
484
545
 
546
+ STRICT RULE — MAXIMUM ONE SOCIAL CALL AT A TIME:
547
+ - You MUST call this tool at most ONCE per turn. Wait for results before deciding if you need another call.
548
+ - NEVER batch multiple social calls in parallel (e.g. search + user profile + posts). Make ONE call, analyze results, then follow up only if needed.
549
+ - If the user says "search social media for X", pick ONE platform (Twitter for crypto/tech news, Reddit for discussions/reviews) — do NOT call both unless explicitly asked.
550
+ - Do NOT fetch user profile + user posts + user followers in separate parallel calls. Start with the most relevant one (usually search or posts).
551
+ - Default num to 10. Do NOT set num > 20 unless the user explicitly asks for more results.
552
+
485
553
  Platform detection:
486
554
  - "twitter" — for tweets, X posts, 推特, 推文, Twitter users, trending
487
555
  - "reddit" — for Reddit posts, subreddit content, Reddit discussions, r/ anything
@@ -501,111 +569,497 @@ Preferences:
501
569
  - Strip @ from Twitter usernames (e.g. @elonmusk → "elonmusk"), strip r/ from subreddits (e.g. r/LocalLLaMA → "LocalLLaMA").
502
570
  - Use "sort": "latest" or "new" when user asks for latest/newest/最新 content.
503
571
  - 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)}`);
572
+ platform: zod_1.z
573
+ .enum(["twitter", "reddit"])
574
+ .describe("Social media platform"),
575
+ type: zod_1.z
576
+ .enum([
577
+ "search",
578
+ "posts",
579
+ "comments",
580
+ "user",
581
+ "detail",
582
+ "latest_comments",
583
+ "replies",
584
+ "media",
585
+ "highlights",
586
+ "followers",
587
+ "following",
588
+ "retweets",
589
+ "trending",
590
+ ])
591
+ .optional()
592
+ .describe("Query type. Default 'search'. See tool description for platform-specific types and required parameters."),
593
+ query: zod_1.z
594
+ .string()
595
+ .optional()
596
+ .describe("Search query (required for type=search)"),
597
+ username: zod_1.z
598
+ .string()
599
+ .optional()
600
+ .describe("Username without @ (for type=posts/user/replies/media/highlights/followers/following)"),
601
+ user_id: zod_1.z
602
+ .string()
603
+ .optional()
604
+ .describe("Twitter user ID (for type=highlights, alternative to username)"),
605
+ post_id: zod_1.z
606
+ .string()
607
+ .optional()
608
+ .describe("Post/tweet ID (for type=detail/comments/latest_comments/retweets)"),
609
+ subreddit: zod_1.z
610
+ .string()
611
+ .optional()
612
+ .describe("Subreddit name without r/ (for Reddit type=posts)"),
613
+ sort: zod_1.z
614
+ .enum(["relevance", "hot", "top", "new", "latest", "comments"])
615
+ .optional()
616
+ .describe("Sort order. Default 'relevance' for search, 'hot' for posts."),
617
+ time: zod_1.z
618
+ .enum(["hour", "day", "week", "month", "year", "all"])
619
+ .optional()
620
+ .describe("Time filter for sort=top results"),
621
+ num: zod_1.z
622
+ .coerce.number()
623
+ .min(1)
624
+ .max(100)
625
+ .optional()
626
+ .describe("Number of results, default 10"),
627
+ country: zod_1.z
628
+ .string()
629
+ .optional()
630
+ .describe("Country for trending topics (Twitter only). Default 'UnitedStates'."),
631
+ cursor: zod_1.z
632
+ .string()
633
+ .optional()
634
+ .describe("Pagination cursor from previous response's next_cursor"),
635
+ }, async ({ platform, type, query, username, user_id, post_id, subreddit, sort, time, num, country, cursor, }) => {
636
+ const body = { platform };
637
+ if (type)
638
+ body.type = type;
639
+ if (query)
640
+ body.query = query;
641
+ if (username)
642
+ body.username = username;
643
+ if (user_id)
644
+ body.user_id = user_id;
645
+ if (post_id)
646
+ body.post_id = post_id;
647
+ if (subreddit)
648
+ body.subreddit = subreddit;
649
+ if (sort)
650
+ body.sort = sort;
651
+ if (time)
652
+ body.time = time;
653
+ if (num)
654
+ body.num = num;
655
+ if (country)
656
+ body.country = country;
657
+ if (cursor)
658
+ body.cursor = cursor;
659
+ try {
660
+ const { ok, data } = await callAPI("/v1/social", body);
661
+ if (!ok) {
662
+ return errorResult(`Social query failed: ${data.error || JSON.stringify(data)}`);
663
+ }
664
+ const results = data.results;
665
+ const count = results?.length ?? 0;
666
+ const hint = count > 0
667
+ ? `\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.]`
668
+ : `\n\n[No results. Try broader keywords or a different type/sort. Do NOT retry the same query.]`;
669
+ const compact = compactResult(data);
670
+ compact.content[0].text += hint;
671
+ return compact;
595
672
  }
596
- return textResult(data);
597
- }
598
- catch (err) {
599
- return errorResult(`Failed to connect to AgentKey server at ${AGENTKEY_BASE_URL}: ${err}`);
600
- }
601
- });
673
+ catch (err) {
674
+ return errorResult(`Failed to connect to AgentKey server at ${AGENTKEY_BASE_URL}: ${err}`);
675
+ }
676
+ });
677
+ // Crypto tool
678
+ server.tool("agentkey_crypto", `Query blockchain and crypto data in real time. Three data sources in one tool:
679
+ (1) Chainbase on-chain data — token holders, wallet balances, NFTs, transactions, ENS domains, DeFi portfolios, smart contract calls, SQL on-chain queries.
680
+ (2) CoinMarketCap market data — crypto rankings, price quotes, market cap, trending coins, gainers/losers, categories, global metrics.
681
+ (3) Crypto social intelligence (Tops) — trending crypto narratives, social mentions, Twitter/X crypto discussions, topic discovery.
682
+
683
+ USE THIS TOOL WHEN the user:
684
+ - Asks about token/coin prices, market cap, rankings (e.g. "BTC price", "top 10 coins", "crypto market cap")
685
+ - Asks about token holders, transfers, on-chain data (e.g. "top USDC holders", "USDT transfers")
686
+ - Asks about NFT collections, floor prices, ownership, rarity, or trending NFTs
687
+ - Asks about wallet balances, token holdings, DeFi portfolio, or transaction history
688
+ - Asks about blockchain data: latest block, block details, transaction details
689
+ - Asks about ENS domains, Space ID domains, or address resolution
690
+ - Asks about trending coins, gainers, losers, or what's hot in crypto markets
691
+ - Asks about crypto categories (DeFi, Layer 1, Meme coins, etc.)
692
+ - Asks about global crypto market metrics, BTC dominance, total market cap
693
+ - Asks to convert between crypto/fiat currencies
694
+ - Asks to query on-chain data with SQL or call a smart contract function
695
+ - Asks about crypto social narratives, mentions, or trending topics
696
+ - Mentions keywords: "token", "coin", "price", "market cap", "holder", "wallet", "balance", "NFT", "block", "transaction", "ENS", "on-chain", "ranking", "trending", "gainers", "losers"
697
+ - Mentions Chinese keywords: "链上", "代币", "币价", "市值", "持币", "钱包", "余额", "持仓", "区块", "交易", "地址", "合约", "涨幅", "跌幅", "排行"
698
+ - Mentions any EVM address (0x...), token contract, transaction hash, or .eth domain
699
+ - Mentions coin symbols like BTC, ETH, SOL, USDT, USDC, etc.
700
+
701
+ Supported chains (Chainbase): Ethereum (1), BSC (56), Polygon (137), Avalanche (43114), Arbitrum (42161), Optimism (10), Base (8453), zkSync (324).
702
+
703
+ Common token contracts (Ethereum):
704
+ - USDT: 0xdAC17F958D2ee523a2206206994597C13D831ec7
705
+ - USDC: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
706
+ - WETH: 0xC02aaA39b223FE8D0A0e5c4F27eAD9083C756Cc2
707
+ - DAI: 0x6B175474E89094C44Da98b954EedeAC495271d0F
708
+ - WBTC: 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599
709
+
710
+ Routing — match user intent to the right type:
711
+
712
+ CoinMarketCap market data (use symbol like "BTC", "ETH" — no contract address needed):
713
+ - "top coins" / "crypto rankings" / "币圈排行" → cmc_listings (sort by market_cap)
714
+ - "BTC price" / "ETH price" / "coin price by name" → cmc_quotes (needs symbol like "BTC,ETH")
715
+ - "what is Bitcoin" / "coin info" / "coin logo" → cmc_info (needs symbol)
716
+ - "total market cap" / "BTC dominance" / "市场总览" → cmc_global_metrics
717
+ - "trending coins" / "热门币种" → cmc_trending
718
+ - "biggest gainers" / "biggest losers" / "涨幅榜" / "跌幅榜" → cmc_gainers_losers (sort_dir: "desc" for gainers, "asc" for losers)
719
+ - "most popular coins" / "most visited" → cmc_most_visited
720
+ - "DeFi category" / "meme coins" / "crypto categories" → cmc_categories or cmc_category (needs category_id)
721
+ - "convert 1 BTC to USD" / "币价换算" → cmc_price_conversion (needs amount, symbol, convert)
722
+ - "find coin by symbol" / "coin ID lookup" → cmc_map (needs symbol)
723
+
724
+ Chainbase on-chain data (use contract_address or wallet address):
725
+ - "on-chain token price" / "token price by contract" → token_price (needs contract_address)
726
+ - "token metadata by contract" → token_metadata (needs contract_address)
727
+ - "price history" / "历史价格" → token_price_history (needs contract_address, from_timestamp, end_timestamp)
728
+ - "who holds" / "top holders" / "持币大户" → token_top_holders (needs contract_address)
729
+ - "token transfers" / "转账记录" → token_transfers (needs contract_address)
730
+ - "wallet balance" / "ETH balance" / "余额" → balance (needs address)
731
+ - "ERC20 holdings" / "持仓" → tokens (needs address)
732
+ - "NFT holdings" → nfts (needs address)
733
+ - "DeFi portfolio" → portfolios (needs address)
734
+ - "latest block" → block_latest
735
+ - "block detail" → block_detail (needs block_number)
736
+ - "transaction detail" → tx_detail (needs hash)
737
+ - "transaction history" → tx_list (needs address)
738
+ - "NFT floor price" → nft_floor_price (needs contract_address)
739
+ - "NFT collection" → nft_collection (needs contract_address)
740
+ - "trending NFT" → nft_collection_trending
741
+ - "ENS → address" → ens_resolve (needs domain)
742
+ - "address → ENS" → ens_reverse (needs address)
743
+ - "address label" → address_labels (needs address)
744
+ - "call contract function" → contract_call (needs contract_address, function_name, abi)
745
+ - "SQL query" → sql_execute (needs sql)
746
+
747
+ Crypto social intelligence (Tops):
748
+ - "what's trending in crypto" / "加密热点" → trending
749
+ - "topic detail" → topic (needs topic_id)
750
+ - "topic posts/tweets" → topic_posts (needs topic_id)
751
+ - "crypto mentions" → mentions (needs query)
752
+ - "narrative search" → tops_search (needs query)
753
+
754
+ Decision guide — CoinMarketCap vs Chainbase:
755
+ - Use cmc_* types when user asks by coin NAME/SYMBOL (BTC, ETH) without a specific contract address
756
+ - Use Chainbase types when user provides a contract address (0x...) or needs on-chain data (holders, transfers, balances, NFTs)
757
+ - For general "what's the price of Bitcoin" → cmc_quotes; for "price of token 0xA0b8..." → token_price
758
+
759
+ SQL fallback: Use sql_execute for complex queries. Common tables: {chain}.blocks, {chain}.transactions, {chain}.token_transfers, {chain}.token_metas, {chain}.logs
760
+
761
+ Preferences:
762
+ - Default chain_id to 1 (Ethereum) for Chainbase types.
763
+ - If user says a token name without contract address, prefer cmc_quotes (by symbol) over token_price (by contract).
764
+ - Default page=1, limit=20 for paginated results.
765
+ - Default convert to "USD" for CoinMarketCap types.`, {
766
+ type: zod_1.z
767
+ .enum([
768
+ "token_price",
769
+ "token_metadata",
770
+ "token_price_history",
771
+ "token_holders",
772
+ "token_top_holders",
773
+ "token_transfers",
774
+ "nft_metadata",
775
+ "nft_collection",
776
+ "nft_collection_items",
777
+ "nft_search",
778
+ "nft_transfers",
779
+ "nft_owner",
780
+ "nft_owners",
781
+ "nft_floor_price",
782
+ "nft_price_history",
783
+ "nft_collection_trending",
784
+ "balance",
785
+ "tokens",
786
+ "nfts",
787
+ "portfolios",
788
+ "block_latest",
789
+ "block_detail",
790
+ "tx_detail",
791
+ "tx_list",
792
+ "ens",
793
+ "ens_resolve",
794
+ "ens_reverse",
795
+ "spaceid_resolve",
796
+ "spaceid_reverse",
797
+ "address_labels",
798
+ "contract_call",
799
+ "sql_execute",
800
+ "trending",
801
+ "topic",
802
+ "topic_posts",
803
+ "mentions",
804
+ "tops_search",
805
+ "cmc_listings",
806
+ "cmc_quotes",
807
+ "cmc_info",
808
+ "cmc_map",
809
+ "cmc_global_metrics",
810
+ "cmc_trending",
811
+ "cmc_gainers_losers",
812
+ "cmc_most_visited",
813
+ "cmc_categories",
814
+ "cmc_category",
815
+ "cmc_price_conversion",
816
+ ])
817
+ .describe("Query type — see tool description for routing guide"),
818
+ chain_id: zod_1.z
819
+ .coerce.number()
820
+ .optional()
821
+ .describe("Blockchain chain ID. Default 1 (Ethereum). Common: BSC=56, Polygon=137, Arbitrum=42161, Base=8453, Optimism=10, Avalanche=43114, zkSync=324."),
822
+ contract_address: zod_1.z
823
+ .string()
824
+ .optional()
825
+ .describe("Token or NFT contract address (0x...)"),
826
+ address: zod_1.z
827
+ .string()
828
+ .optional()
829
+ .describe("Wallet address (0x...) for balance, tx, ENS reverse, etc."),
830
+ token_id: zod_1.z
831
+ .string()
832
+ .optional()
833
+ .describe("NFT token ID within a collection"),
834
+ hash: zod_1.z.string().optional().describe("Transaction hash for tx_detail"),
835
+ block_number: zod_1.z
836
+ .coerce.number()
837
+ .optional()
838
+ .describe("Block number for block_detail"),
839
+ domain: zod_1.z
840
+ .string()
841
+ .optional()
842
+ .describe("ENS or Space ID domain name (e.g. 'vitalik.eth')"),
843
+ query: zod_1.z
844
+ .string()
845
+ .optional()
846
+ .describe("Search keyword for nft_search, mentions, or tops_search"),
847
+ name: zod_1.z.string().optional().describe("NFT collection name for nft_search"),
848
+ topic_id: zod_1.z
849
+ .string()
850
+ .optional()
851
+ .describe("Topic ID for topic/topic_posts (from trending results)"),
852
+ sql: zod_1.z
853
+ .string()
854
+ .optional()
855
+ .describe("SQL query for sql_execute. Example: SELECT * FROM ethereum.blocks ORDER BY number DESC LIMIT 5"),
856
+ function_name: zod_1.z
857
+ .string()
858
+ .optional()
859
+ .describe("Smart contract function name for contract_call (e.g. 'balanceOf')"),
860
+ abi: zod_1.z
861
+ .string()
862
+ .optional()
863
+ .describe("Contract ABI as JSON string for contract_call"),
864
+ params: zod_1.z
865
+ .array(zod_1.z.string())
866
+ .optional()
867
+ .describe("Function parameters for contract_call (array of strings)"),
868
+ from_block: zod_1.z.coerce.number().optional().describe("Start block number"),
869
+ to_block: zod_1.z.coerce.number().optional().describe("End block number"),
870
+ from_timestamp: zod_1.z
871
+ .coerce.number()
872
+ .optional()
873
+ .describe("Start timestamp (unix seconds)"),
874
+ end_timestamp: zod_1.z
875
+ .coerce.number()
876
+ .optional()
877
+ .describe("End timestamp (unix seconds)"),
878
+ sort: zod_1.z
879
+ .string()
880
+ .optional()
881
+ .describe("Sort order for applicable queries"),
882
+ page: zod_1.z
883
+ .coerce.number()
884
+ .min(1)
885
+ .optional()
886
+ .describe("Page number, default 1"),
887
+ limit: zod_1.z
888
+ .coerce.number()
889
+ .min(1)
890
+ .max(100)
891
+ .optional()
892
+ .describe("Results per page, default 20"),
893
+ language: zod_1.z
894
+ .string()
895
+ .optional()
896
+ .describe("Language for trending topics (e.g. 'en', 'zh')"),
897
+ // CoinMarketCap params
898
+ symbol: zod_1.z
899
+ .string()
900
+ .optional()
901
+ .describe("Comma-separated coin symbols for cmc_* types (e.g. 'BTC,ETH'). Use this instead of contract_address for CoinMarketCap queries."),
902
+ slug: zod_1.z
903
+ .string()
904
+ .optional()
905
+ .describe("Comma-separated coin slugs (e.g. 'bitcoin,ethereum')"),
906
+ cmc_id: zod_1.z
907
+ .string()
908
+ .optional()
909
+ .describe("Comma-separated CoinMarketCap IDs"),
910
+ convert: zod_1.z
911
+ .string()
912
+ .optional()
913
+ .describe("Conversion currency for cmc_* types (e.g. 'USD', 'EUR', 'BTC'). Default 'USD'."),
914
+ category_id: zod_1.z
915
+ .string()
916
+ .optional()
917
+ .describe("CoinMarketCap category ID for cmc_category"),
918
+ time_period: zod_1.z
919
+ .enum(["1h", "24h", "7d", "30d"])
920
+ .optional()
921
+ .describe("Time period for cmc_trending/cmc_gainers_losers/cmc_most_visited. Default '24h'."),
922
+ sort_dir: zod_1.z
923
+ .enum(["asc", "desc"])
924
+ .optional()
925
+ .describe("Sort direction. For cmc_gainers_losers: 'desc'=gainers first, 'asc'=losers first."),
926
+ amount: zod_1.z
927
+ .coerce.number()
928
+ .optional()
929
+ .describe("Amount for cmc_price_conversion (e.g. 1.5)"),
930
+ cryptocurrency_type: zod_1.z
931
+ .enum(["all", "coins", "tokens"])
932
+ .optional()
933
+ .describe("Filter for cmc_listings: 'all', 'coins', or 'tokens'"),
934
+ tag: zod_1.z
935
+ .string()
936
+ .optional()
937
+ .describe("Filter tag for cmc_listings (e.g. 'defi', 'filesharing')"),
938
+ price_min: zod_1.z.coerce.number().optional().describe("Min USD price filter for cmc_listings"),
939
+ price_max: zod_1.z.coerce.number().optional().describe("Max USD price filter for cmc_listings"),
940
+ market_cap_min: zod_1.z.coerce.number().optional().describe("Min market cap filter for cmc_listings"),
941
+ market_cap_max: zod_1.z.coerce.number().optional().describe("Max market cap filter for cmc_listings"),
942
+ volume_24h_min: zod_1.z.coerce.number().optional().describe("Min 24h volume filter for cmc_listings"),
943
+ volume_24h_max: zod_1.z.coerce.number().optional().describe("Max 24h volume filter for cmc_listings"),
944
+ }, 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, }) => {
945
+ const body = { type };
946
+ if (chain_id)
947
+ body.chain_id = chain_id;
948
+ if (contract_address)
949
+ body.contract_address = contract_address;
950
+ if (address)
951
+ body.address = address;
952
+ if (token_id)
953
+ body.token_id = token_id;
954
+ if (hash)
955
+ body.hash = hash;
956
+ if (block_number)
957
+ body.block_number = block_number;
958
+ if (domain)
959
+ body.domain = domain;
960
+ if (query)
961
+ body.query = query;
962
+ if (name)
963
+ body.name = name;
964
+ if (topic_id)
965
+ body.topic_id = topic_id;
966
+ if (sql)
967
+ body.sql = sql;
968
+ if (function_name)
969
+ body.function_name = function_name;
970
+ if (abi)
971
+ body.abi = abi;
972
+ if (params?.length)
973
+ body.params = params;
974
+ if (from_block)
975
+ body.from_block = from_block;
976
+ if (to_block)
977
+ body.to_block = to_block;
978
+ if (from_timestamp)
979
+ body.from_timestamp = from_timestamp;
980
+ if (end_timestamp)
981
+ body.end_timestamp = end_timestamp;
982
+ if (sort)
983
+ body.sort = sort;
984
+ if (page)
985
+ body.page = page;
986
+ if (limit)
987
+ body.limit = limit;
988
+ if (language)
989
+ body.language = language;
990
+ if (symbol)
991
+ body.symbol = symbol;
992
+ if (slug)
993
+ body.slug = slug;
994
+ if (cmc_id)
995
+ body.cmc_id = cmc_id;
996
+ if (convert)
997
+ body.convert = convert;
998
+ if (category_id)
999
+ body.category_id = category_id;
1000
+ if (time_period)
1001
+ body.time_period = time_period;
1002
+ if (sort_dir)
1003
+ body.sort_dir = sort_dir;
1004
+ if (amount)
1005
+ body.amount = amount;
1006
+ if (cryptocurrency_type)
1007
+ body.cryptocurrency_type = cryptocurrency_type;
1008
+ if (tag)
1009
+ body.tag = tag;
1010
+ if (price_min)
1011
+ body.price_min = price_min;
1012
+ if (price_max)
1013
+ body.price_max = price_max;
1014
+ if (market_cap_min)
1015
+ body.market_cap_min = market_cap_min;
1016
+ if (market_cap_max)
1017
+ body.market_cap_max = market_cap_max;
1018
+ if (volume_24h_min)
1019
+ body.volume_24h_min = volume_24h_min;
1020
+ if (volume_24h_max)
1021
+ body.volume_24h_max = volume_24h_max;
1022
+ try {
1023
+ const { ok, data } = await callAPI("/v1/crypto", body);
1024
+ if (!ok) {
1025
+ return errorResult(`Crypto query failed: ${data.error || JSON.stringify(data)}`);
1026
+ }
1027
+ const compact = compactResult(data);
1028
+ compact.content[0].text +=
1029
+ `\n\n[Data returned. Present the results to the user. Do NOT make follow-up crypto calls unless the user asks for additional data.]`;
1030
+ return compact;
1031
+ }
1032
+ catch (err) {
1033
+ return errorResult(`Failed to connect to AgentKey server at ${AGENTKEY_BASE_URL}: ${err}`);
1034
+ }
1035
+ });
1036
+ } // end registerBuiltinTools()
602
1037
  // --- Start ---
1038
+ const POLL_INTERVAL_MS = parseInt(process.env.AGENTKEY_POLL_INTERVAL || "30000", 10);
603
1039
  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");
1040
+ let dynamicManager = null;
1041
+ // Try dynamic tool loading from /v1/tools first
1042
+ const registry = await (0, dynamic_js_1.fetchToolRegistry)(AGENTKEY_BASE_URL);
1043
+ if (registry && registry.tools.length > 0) {
1044
+ dynamicManager = new dynamic_js_1.DynamicToolManager(server, callAPI, AGENTKEY_BASE_URL, POLL_INTERVAL_MS);
1045
+ const count = dynamicManager.registerAll(registry);
1046
+ dynamicManager.startPolling();
1047
+ console.error(`AgentKey MCP Server running on stdio (dynamic: ${count} tools, hot-reload every ${POLL_INTERVAL_MS / 1000}s)`);
1048
+ }
1049
+ else {
1050
+ // REST API server not reachable — fall back to hardcoded tool definitions.
1051
+ registerBuiltinTools();
1052
+ console.error("AgentKey MCP Server running on stdio (using built-in tool definitions)");
1053
+ }
607
1054
  console.error(` API: ${AGENTKEY_BASE_URL}`);
608
1055
  console.error(` Key: ${AGENTKEY_API_KEY ? "****" + AGENTKEY_API_KEY.slice(-4) : "(not set)"}`);
1056
+ const transport = new stdio_js_1.StdioServerTransport();
1057
+ await server.connect(transport);
1058
+ // Cleanup on shutdown
1059
+ const cleanup = () => {
1060
+ dynamicManager?.stopPolling();
1061
+ };
1062
+ process.on("beforeExit", cleanup);
609
1063
  }
610
1064
  // Graceful shutdown
611
1065
  process.on("SIGINT", () => {