kiku-agent 1.0.11 → 1.2.0

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.
Files changed (221) hide show
  1. package/README.md +300 -495
  2. package/package.json +12 -11
  3. package/public/fonts/GoogleSans-Bold.ttf +0 -0
  4. package/public/fonts/GoogleSans-Bold.woff2 +0 -0
  5. package/public/fonts/GoogleSans-BoldItalic.ttf +0 -0
  6. package/public/fonts/GoogleSans-Italic.ttf +0 -0
  7. package/public/fonts/GoogleSans-Latin-400.woff2 +0 -0
  8. package/public/fonts/GoogleSans-Latin-500.woff2 +0 -0
  9. package/public/fonts/GoogleSans-Latin-600.woff2 +0 -0
  10. package/public/fonts/GoogleSans-Latin-700.woff2 +0 -0
  11. package/public/fonts/GoogleSans-Medium.ttf +0 -0
  12. package/public/fonts/GoogleSans-Medium.woff2 +0 -0
  13. package/public/fonts/GoogleSans-Regular.ttf +0 -0
  14. package/public/fonts/GoogleSans-Regular.woff2 +0 -0
  15. package/public/fonts/GoogleSans-Thai-400.woff2 +0 -0
  16. package/public/fonts/GoogleSans-Thai-500.woff2 +0 -0
  17. package/public/fonts/GoogleSans-Thai-600.woff2 +0 -0
  18. package/public/fonts/GoogleSans-Thai-700.woff2 +0 -0
  19. package/public/fonts/NotoSansThai-Bold.woff2 +0 -0
  20. package/public/fonts/NotoSansThai-Regular.woff2 +0 -0
  21. package/public/fonts/google-sans-official.css +900 -0
  22. package/public/images/0040jbadgy1id6am4yxqfj61s82dn1kx02.jpg +0 -0
  23. package/public/images/0040jbadgy1id6am708pbj62c0340qv602.jpg +0 -0
  24. package/public/images/0040jbadgy1id6amb81uqj62c0340x6q02.jpg +0 -0
  25. package/public/images/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
  26. package/public/images/0040jbadgy1id6amdg3xcj62b232wqv502.jpg +0 -0
  27. package/public/images/0040jbadgy1id6amexv23j620v2pau0x02.jpg +0 -0
  28. package/public/images/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
  29. package/public/images/008ET7Legy1ig9s89nri5j30i20n1gn4.jpg +0 -0
  30. package/public/images/008ET7Legy1ig9s8a2lmlj30i20ny409.jpg +0 -0
  31. package/public/images/008ET7Legy1ig9s8ay1hsj30i20nk3zv.jpg +0 -0
  32. package/public/images/008ET7Legy1ig9s8bab6sj30i20o7go1.jpg +0 -0
  33. package/public/images/008ET7Legy1ig9s8blqsxj30i20oiq4r.jpg +0 -0
  34. package/public/images/008ET7Legy1ig9s8cbxkzj30i20mxmzh.jpg +0 -0
  35. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-399915.jpg +0 -0
  36. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-424041.jpg +0 -0
  37. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-588166.jpg +0 -0
  38. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-672198.jpg +0 -0
  39. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-700522.jpg +0 -0
  40. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-839722.jpg +0 -0
  41. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026825-460586.jpg +0 -0
  42. package/public/images/douyin-update/thumbs//346/212/226/351/237/263202694-689407.jpg +0 -0
  43. package/public/images/douyin-update//346/212/226/351/237/2632026820-399915.mp4 +0 -0
  44. package/public/images/douyin-update//346/212/226/351/237/2632026820-424041.mp4 +0 -0
  45. package/public/images/douyin-update//346/212/226/351/237/2632026820-588166.mp4 +0 -0
  46. package/public/images/douyin-update//346/212/226/351/237/2632026820-672198.mp4 +0 -0
  47. package/public/images/douyin-update//346/212/226/351/237/2632026820-700522.mp4 +0 -0
  48. package/public/images/douyin-update//346/212/226/351/237/2632026820-839722.mp4 +0 -0
  49. package/public/images/douyin-update//346/212/226/351/237/2632026825-460586.mp4 +0 -0
  50. package/public/images/douyin-update//346/212/226/351/237/263202694-689407.mp4 +0 -0
  51. package/public/images/general/0040jbadgy1id6am4yxqfj61s82dn1kx02.jpg +0 -0
  52. package/public/images/general/0040jbadgy1id6am708pbj62c0340qv602.jpg +0 -0
  53. package/public/images/general/0040jbadgy1id6amb81uqj62c0340x6q02.jpg +0 -0
  54. package/public/images/general/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
  55. package/public/images/general/0040jbadgy1id6amdg3xcj62b232wqv502.jpg +0 -0
  56. package/public/images/general/0040jbadgy1id6amexv23j620v2pau0x02.jpg +0 -0
  57. package/public/images/general/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
  58. package/public/images/general/008ET7Legy1ig9s89nri5j30i20n1gn4.jpg +0 -0
  59. package/public/images/general/008ET7Legy1ig9s8a2lmlj30i20ny409.jpg +0 -0
  60. package/public/images/general/008ET7Legy1ig9s8ay1hsj30i20nk3zv.jpg +0 -0
  61. package/public/images/general/008ET7Legy1ig9s8bab6sj30i20o7go1.jpg +0 -0
  62. package/public/images/general/008ET7Legy1ig9s8blqsxj30i20oiq4r.jpg +0 -0
  63. package/public/images/general/008ET7Legy1ig9s8cbxkzj30i20mxmzh.jpg +0 -0
  64. package/public/images/general/jujingyi_portrait.jpg +0 -0
  65. package/public/images/jujingyi_portrait.jpg +0 -0
  66. package/public/images/series/a-millenniums-wait/poster.jpg +0 -0
  67. package/public/images/series/beauty-of-resilience/poster.jpg +0 -0
  68. package/public/images/series/detective-samoyeds/poster.jpg +0 -0
  69. package/public/images/series/everyday/poster.jpg +0 -0
  70. package/public/images/series/fu-rong-ancient-painting/poster.jpg +0 -0
  71. package/public/images/series/in-a-class-of-her-own/poster.jpg +0 -0
  72. package/public/images/series/in-blossom/poster.jpg +0 -0
  73. package/public/images/series/legend-of-yunxi/poster.jpg +0 -0
  74. package/public/images/series/lets-fight/poster-2.jpg +0 -0
  75. package/public/images/series/lets-fight/poster.jpg +0 -0
  76. package/public/images/series/love-emergency/poster.jpg +0 -0
  77. package/public/images/series/love-under-the-full-moon/poster.jpg +0 -0
  78. package/public/images/series/money-bastion/poster.jpg +0 -0
  79. package/public/images/series/mr-swimmer/poster.jpg +0 -0
  80. package/public/images/series/novoland-castle-in-the-sky/poster.jpg +0 -0
  81. package/public/images/series/please-give-me-a-pair-of-wings/poster.jpg +0 -0
  82. package/public/images/series/rebirth-for-you/poster.jpg +0 -0
  83. package/public/images/series/sword-and-fairy-4/poster.jpg +0 -0
  84. package/public/images/series/the-blooms-at-ruyi-pavilion/poster.jpg +0 -0
  85. package/public/images/series/the-eternal-fragrance/poster-2.jpg +0 -0
  86. package/public/images/series/the-eternal-fragrance/poster.jpg +0 -0
  87. package/public/images/series/the-fallen-flowers/poster.jpg +0 -0
  88. package/public/images/series/the-legend-of-white-snake/poster.jpg +0 -0
  89. package/public/images/series/veil-of-shadows/poster.jpg +0 -0
  90. package/public/images/series/wanhua-sin/poster-2.jpg +0 -0
  91. package/public/images/series/wanhua-sin/poster.jpg +0 -0
  92. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj10yogj62u03s0npe02.jpg +0 -0
  93. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj2pdvyj61li25jkjl02.jpg +0 -0
  94. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj71177j62672w9e8302.jpg +0 -0
  95. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj7twpfj62192197rw02.jpg +0 -0
  96. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj8jfu8j62c03401kx02.jpg +0 -0
  97. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj9dhdsj622k2l77wh02.jpg +0 -0
  98. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkja31s4j62bw3407r802.jpg +0 -0
  99. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjcf331j61402ey1kx02.jpg +0 -0
  100. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjdx8gqj62602pihdt02.jpg +0 -0
  101. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjfvxnzj62432n57wh02.jpg +0 -0
  102. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixniv2yj627y4fxb2b02.jpg +0 -0
  103. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixsvbdaj625k2vfu1102.jpg +0 -0
  104. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixxxdaoj62c03501l202.jpg +0 -0
  105. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixzeef9j62c0340npd02.jpg +0 -0
  106. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3iy0vs6tj626t4rbhdu02.jpg +0 -0
  107. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3iy21wwuj62bt34hx6q02.jpg +0 -0
  108. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly4wr85j62z629y7wh02.jpg +0 -0
  109. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly67oilj623u2t5npd02.jpg +0 -0
  110. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly9qtw8j62c0340u0x02.jpg +0 -0
  111. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlyem4m4j63402c01kz02.jpg +0 -0
  112. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlygkoq4j627s2yeavd02.jpg +0 -0
  113. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlyjh0daj62bw340nm102.jpg +0 -0
  114. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlylejlhj62bw3401kx02.jpg +0 -0
  115. package/public/images/weibo-update/today/0040jbadgy1igaiaa8b7fj627j2y67wh02.jpg +0 -0
  116. package/public/images/weibo-update/today/0040jbadgy1igaiagw3ezj61s02ebu0x02.jpg +0 -0
  117. package/public/images/weibo-update/today/0040jbadgy1igaiaivng3j61rv2ebx6p02.jpg +0 -0
  118. package/public/images/weibo-update/today/0040jbadgy1igaiajuixvj62bw340qv502.jpg +0 -0
  119. package/public/images/weibo-update/today/0040jbadgy1igaiapyartj62bw340hdt02.jpg +0 -0
  120. package/public/images/weibo-update/today/0040jbadgy1igaibkwu4ij61vy2iq1kx02.jpg +0 -0
  121. package/public/images/weibo-update/today/0040jbadgy1igaic1fvkpj61u92eo1ky02.jpg +0 -0
  122. package/public/images/weibo-update/today/0040jbadgy1igaic3cruhj62822ywb2902.jpg +0 -0
  123. package/public/manifest.json +25 -0
  124. package/public/mcp-logo-128.png +0 -0
  125. package/public/mcp-logo-64.png +0 -0
  126. package/public/mcp-logo.svg +16 -0
  127. package/server/agent/agentCore.js +542 -74
  128. package/server/agent/appPaths.js +1 -0
  129. package/server/agent/audioPlayer.js +4 -2
  130. package/server/agent/citations.js +294 -0
  131. package/server/agent/clarificationAnalytics.js +477 -0
  132. package/server/agent/clarifyingQuestions.js +1089 -0
  133. package/server/agent/cli.js +549 -125
  134. package/server/agent/commandFeatures.js +1 -0
  135. package/server/agent/commandRegistry.js +946 -879
  136. package/server/agent/commands.js +546 -272
  137. package/server/agent/config.js +581 -412
  138. package/server/agent/contextCompactor.js +2 -2
  139. package/server/agent/debate.js +235 -0
  140. package/server/agent/eventBus.js +3 -0
  141. package/server/agent/feedbackLearning.js +217 -0
  142. package/server/agent/llmClient.js +512 -394
  143. package/server/agent/mcpClient.js +182 -32
  144. package/server/agent/memory.js +283 -224
  145. package/server/agent/modelManager.js +79 -39
  146. package/server/agent/profiles.js +178 -0
  147. package/server/agent/publicTools.js +22 -0
  148. package/server/agent/queryDetails.js +68 -8
  149. package/server/agent/quizData.js +398 -384
  150. package/server/agent/research.js +121 -79
  151. package/server/agent/sanitizer.js +104 -13
  152. package/server/agent/session.js +319 -11
  153. package/server/agent/subagent.js +8 -3
  154. package/server/agent/suggestionTui.js +294 -0
  155. package/server/agent/suggestions.js +69 -3
  156. package/server/agent/telemetry.js +119 -0
  157. package/server/agent/terminalImage.js +146 -27
  158. package/server/agent/theme.js +17 -0
  159. package/server/agent/toolRouter.js +16 -1
  160. package/server/agent/tools.js +424 -225
  161. package/server/agent/ui.js +486 -64
  162. package/server/agent/version.js +2 -4
  163. package/server/agent/webSearch.js +111 -11
  164. package/server/agent/youtubeAuth.js +24 -6
  165. package/server/apiRouter.js +4222 -0
  166. package/server/mcp/data/discography.js +25 -16
  167. package/server/mcp/data/gallery.js +308 -20
  168. package/server/mcp/data/profile.js +74 -44
  169. package/server/mcp/data/timeline.js +1 -1
  170. package/server/mcp/data/works.js +51 -17
  171. package/server/mcp/dataAdapter.js +5 -3
  172. package/server/mcp/handler.js +5 -11
  173. package/server/mcp/prompts.js +86 -0
  174. package/server/mcp/security.js +18 -15
  175. package/server/mcp/server.js +7 -2
  176. package/server/mcp/tools.js +66 -2
  177. package/server/web-chat/core/browserLauncher.js +18 -0
  178. package/server/web-chat/core/connection.js +194 -0
  179. package/server/web-chat/core/conversationContext.js +82 -0
  180. package/server/web-chat/core/directCatalogEngine.js +196 -0
  181. package/server/web-chat/core/dramaKnowledge.js +142 -0
  182. package/server/web-chat/core/errorHandler.js +51 -0
  183. package/server/web-chat/core/followUpEngine.js +182 -0
  184. package/server/web-chat/core/futureEventGuard.js +80 -0
  185. package/server/web-chat/core/memorySnapshot.js +12 -0
  186. package/server/web-chat/core/security.js +146 -0
  187. package/server/web-chat/core/toolInspector.js +52 -0
  188. package/server/web-chat/router.js +120 -0
  189. package/server/web-chat/routes/chatRoute.js +397 -0
  190. package/server/web-chat/routes/clarifyConfigRoute.js +98 -0
  191. package/server/web-chat/routes/clarifyRoute.js +83 -0
  192. package/server/web-chat/routes/compareRoute.js +79 -0
  193. package/server/web-chat/routes/exportRoute.js +59 -0
  194. package/server/web-chat/routes/feedbackRoute.js +57 -0
  195. package/server/web-chat/routes/newsRoute.js +33 -0
  196. package/server/web-chat/routes/quizRoute.js +11 -0
  197. package/server/web-chat/routes/staticRoute.js +103 -0
  198. package/server/web-chat/routes/statusRoute.js +52 -0
  199. package/server/web-chat/routes/suggestRoute.js +77 -0
  200. package/server/web-chat/routes/timelineRoute.js +34 -0
  201. package/server/web-chat/routes/verifyRoute.js +57 -0
  202. package/server/web-chat/server.js +116 -0
  203. package/server/web-chat/ui/htmlBuilder.js +91 -0
  204. package/server/web-chat/ui/previews/agentBubble.preview.html +134 -0
  205. package/server/web-chat/ui/scripts/agentBubbleSystem.js +320 -0
  206. package/server/web-chat/ui/scripts/client.js +8397 -0
  207. package/server/web-chat/ui/scripts/kikuDb.js +177 -0
  208. package/server/web-chat/ui/scripts/suggestionsData.js +118 -0
  209. package/server/web-chat/ui/scripts/webchatI18n.js +291 -0
  210. package/server/web-chat/ui/styles/main.css +8568 -0
  211. package/server/web-chat/ui/templates/body.html +796 -0
  212. package/server/webChatHtml.js +1 -0
  213. package/server/webChatServer.js +6 -0
  214. package/src/data/discographyData.js +892 -866
  215. package/src/data/galleryData.js +1805 -561
  216. package/src/data/profileData.js +79 -2
  217. package/src/data/socialUpdatesData.js +106 -47
  218. package/src/data/timelineData.js +1 -1
  219. package/src/data/worksData.js +25 -4
  220. package/tokens.css +97 -0
  221. package/server/agent/updateManager.js +0 -456
@@ -7,6 +7,26 @@ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
7
7
  import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
8
8
  import { KIKU_DATA_DIR } from './appPaths.js';
9
9
 
10
+ function getPredefinedKikuMcpTools() {
11
+ return [
12
+ { name: 'search_works', description: 'Search drama and film works by year, title, or role', inputSchema: { type: 'object', properties: { query: { type: 'string' }, year: { type: 'number' } } } },
13
+ { name: 'get_work_detail', description: 'Get detailed information about a specific drama or film', inputSchema: { type: 'object', properties: { title: { type: 'string' } }, required: ['title'] } },
14
+ { name: 'get_character_roles', description: 'Explore character roles played by Ju Jingyi', inputSchema: { type: 'object', properties: { role: { type: 'string' } } } },
15
+ { name: 'search_discography', description: 'Search singles, albums, OSTs, and songs', inputSchema: { type: 'object', properties: { query: { type: 'string' }, type: { type: 'string' } } } },
16
+ { name: 'get_music_videos', description: 'Find official music videos, dance practices, and live stages', inputSchema: { type: 'object', properties: { query: { type: 'string' } } } },
17
+ { name: 'get_media_links', description: 'Get streaming and watch links across YouTube, Bilibili, and QQ Music', inputSchema: { type: 'object', properties: { title: { type: 'string' } } } },
18
+ { name: 'get_brand_endorsements', description: 'Look up global brand ambassadorships and fashion partnerships', inputSchema: { type: 'object', properties: { brand: { type: 'string' }, tier: { type: 'string' } } } },
19
+ { name: 'get_career_timeline', description: 'Explore milestones from SNH48 debut to solo career', inputSchema: { type: 'object', properties: { era: { type: 'string' }, year: { type: 'number' } } } },
20
+ { name: 'get_awards', description: 'Look up music awards, acting honors, and Weibo Night recognitions', inputSchema: { type: 'object', properties: { query: { type: 'string' }, year: { type: 'number' } } } },
21
+ { name: 'get_profile_and_fandom', description: 'Get biographical details, fandom facts, and official links', inputSchema: { type: 'object', properties: { topic: { type: 'string' } } } },
22
+ { name: 'get_quotes', description: 'Get famous quotes and inspirational sayings', inputSchema: { type: 'object', properties: { topic: { type: 'string' } } } },
23
+ { name: 'get_social_feed', description: 'Fetch official social media posts and updates', inputSchema: { type: 'object', properties: { platform: { type: 'string' }, limit: { type: 'number' } } } },
24
+ { name: 'get_gallery_photos', description: 'Search gallery photoshoot pictures and posters', inputSchema: { type: 'object', properties: { query: { type: 'string' }, tag: { type: 'string' } } } },
25
+ { name: 'global_search', description: 'Universal search across dramas, songs, brands, and awards', inputSchema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] } },
26
+ { name: 'get_system_stats', description: 'View knowledge base statistics and metrics', inputSchema: { type: 'object', properties: {} } },
27
+ ];
28
+ }
29
+
10
30
  export class McpClientManager {
11
31
  constructor(options = {}) {
12
32
  this.clientInfo = options.clientInfo || { name: 'kiku-agent', version: PACKAGE_VERSION };
@@ -22,6 +42,40 @@ export class McpClientManager {
22
42
  this.persistedConfigs = new Map();
23
43
  this.configSources = new Map();
24
44
  this.toolRegistry = new Map();
45
+ this.cooldowns = new Map();
46
+ }
47
+
48
+ _isRateLimited(serverName) {
49
+ const cd = this.cooldowns.get(serverName);
50
+ if (!cd) return false;
51
+ if (Date.now() < cd.until) return true;
52
+ this.cooldowns.delete(serverName);
53
+ return false;
54
+ }
55
+
56
+ _registerRateLimit(serverName, errorMessage, { logWarning = false } = {}) {
57
+ const is429 = String(errorMessage).includes('429') ||
58
+ /rate limit|too many requests/i.test(String(errorMessage));
59
+ if (!is429) return false;
60
+
61
+ let retrySec = 45;
62
+ try {
63
+ const match = String(errorMessage).match(/"retryAfterSeconds"\s*:\s*(\d+)/i) ||
64
+ String(errorMessage).match(/(\d+)\s*seconds?/i);
65
+ if (match && match[1]) retrySec = Math.min(Math.max(parseInt(match[1], 10), 10), 300);
66
+ } catch {}
67
+
68
+ const existing = this.cooldowns.get(serverName);
69
+ const now = Date.now();
70
+ const until = now + retrySec * 1000;
71
+
72
+ if (!existing || now >= existing.until) {
73
+ this.cooldowns.set(serverName, { until, warned: true, error: errorMessage });
74
+ if (logWarning) {
75
+ console.warn(`[MCP] Notice: Server "${serverName}" hit rate limit (429). Cooldown for ${retrySec}s — requests paused to avoid spam.`);
76
+ }
77
+ }
78
+ return true;
25
79
  }
26
80
 
27
81
  _trackConfigSource(serverName, filePath) {
@@ -58,12 +112,30 @@ export class McpClientManager {
58
112
  return changed;
59
113
  }
60
114
 
61
- async connect(serverName, serverConfig = {}, { persist = true } = {}) {
115
+ async _ensureConnected(serverName) {
116
+ const entry = this.servers.get(serverName);
117
+ if (entry && entry.client && !entry.isLazy) {
118
+ return entry;
119
+ }
120
+ const cfg = this.persistedConfigs.get(serverName);
121
+ if (!cfg) {
122
+ throw new Error(`MCP server "${serverName}" configuration not found.`);
123
+ }
124
+ await this.connect(serverName, cfg, { persist: false, fetchMetadata: false });
125
+ return this.servers.get(serverName);
126
+ }
127
+
128
+ async connect(serverName, serverConfig = {}, { persist = true, fetchMetadata = false } = {}) {
62
129
  const name = String(serverName || '').trim();
63
130
  if (!name) throw new Error('Server name is required to connect to MCP.');
64
131
 
65
132
  if (this.servers.has(name)) {
66
- await this.disconnect(name, { persist: false });
133
+ const existing = this.servers.get(name);
134
+ if (existing.client) {
135
+ await this.disconnect(name, { persist: false });
136
+ } else {
137
+ this.servers.delete(name);
138
+ }
67
139
  }
68
140
 
69
141
  let transport;
@@ -109,28 +181,39 @@ export class McpClientManager {
109
181
  try {
110
182
  await client.connect(transport);
111
183
 
112
- let tools = [];
113
- let resources = [];
114
- let prompts = [];
184
+ let tools = serverConfig.tools || [];
185
+ let resources = serverConfig.resources || [];
186
+ let prompts = serverConfig.prompts || [];
115
187
 
116
- try {
117
- const toolsResult = await client.listTools();
118
- tools = toolsResult.tools || [];
119
- } catch (err) {
120
- console.warn(`[MCP] Failed to list tools from ${name}: ${err.message}`);
188
+ // If tools not pre-provided, fetch once upon connection
189
+ if (!tools || tools.length === 0) {
190
+ try {
191
+ if (!this._isRateLimited(name)) {
192
+ const toolsResult = await client.listTools();
193
+ tools = toolsResult.tools || [];
194
+ }
195
+ } catch (err) {
196
+ this._registerRateLimit(name, err.message, { logWarning: false });
197
+ }
121
198
  }
122
199
 
123
- try {
124
- const resResult = await client.listResources();
125
- resources = resResult.resources || [];
126
- } catch {}
200
+ if (fetchMetadata) {
201
+ try {
202
+ if (!this._isRateLimited(name)) {
203
+ const resResult = await client.listResources();
204
+ resources = resResult.resources || [];
205
+ }
206
+ } catch {}
127
207
 
128
- try {
129
- const promptResult = await client.listPrompts();
130
- prompts = promptResult.prompts || [];
131
- } catch {}
208
+ try {
209
+ if (!this._isRateLimited(name)) {
210
+ const promptResult = await client.listPrompts();
211
+ prompts = promptResult.prompts || [];
212
+ }
213
+ } catch {}
214
+ }
132
215
 
133
- const cleanConfig = { ...serverConfig, enabled: true };
216
+ const cleanConfig = { ...serverConfig, enabled: true, tools };
134
217
  delete cleanConfig.lastError;
135
218
 
136
219
  const serverEntry = {
@@ -142,6 +225,7 @@ export class McpClientManager {
142
225
  resources,
143
226
  prompts,
144
227
  connectedAt: new Date().toISOString(),
228
+ isLazy: false,
145
229
  };
146
230
 
147
231
  this.servers.set(name, serverEntry);
@@ -173,8 +257,8 @@ export class McpClientManager {
173
257
  if (!entry) return false;
174
258
 
175
259
  try {
176
- await entry.client.close().catch(() => {});
177
- await entry.transport.close().catch(() => {});
260
+ if (entry.client) await entry.client.close().catch(() => {});
261
+ if (entry.transport) await entry.transport.close().catch(() => {});
178
262
  if (entry.transport?._process) {
179
263
  try {
180
264
  entry.transport._process.kill('SIGKILL');
@@ -310,8 +394,33 @@ export class McpClientManager {
310
394
  };
311
395
  }
312
396
 
313
- const serverEntry = this.servers.get(registration.serverName);
314
- if (!serverEntry) {
397
+ if (this._isRateLimited(registration.serverName)) {
398
+ const cd = this.cooldowns.get(registration.serverName);
399
+ const remainingSec = Math.max(1, Math.ceil((cd.until - Date.now()) / 1000));
400
+ return {
401
+ status: 'rate_limited',
402
+ server: registration.serverName,
403
+ tool: registration.originalName,
404
+ message: `MCP server "${registration.serverName}" is currently in rate-limit cooldown (${remainingSec}s remaining). Requests are paused to prevent rate limiting.`,
405
+ };
406
+ }
407
+
408
+ let serverEntry = this.servers.get(registration.serverName);
409
+ if (!serverEntry || !serverEntry.client || serverEntry.isLazy) {
410
+ try {
411
+ serverEntry = await this._ensureConnected(registration.serverName);
412
+ } catch (connErr) {
413
+ this._registerRateLimit(registration.serverName, connErr.message, { logWarning: false });
414
+ return {
415
+ status: 'error',
416
+ server: registration.serverName,
417
+ tool: registration.originalName,
418
+ message: `Could not connect to MCP server "${registration.serverName}": ${connErr.message}`,
419
+ };
420
+ }
421
+ }
422
+
423
+ if (!serverEntry || !serverEntry.client) {
315
424
  return {
316
425
  status: 'error',
317
426
  message: `MCP server "${registration.serverName}" is disconnected.`,
@@ -358,6 +467,7 @@ export class McpClientManager {
358
467
  _rawMcp: response.content,
359
468
  };
360
469
  } catch (err) {
470
+ this._registerRateLimit(registration.serverName, err.message, { logWarning: false });
361
471
  return {
362
472
  status: 'error',
363
473
  server: registration.serverName,
@@ -373,14 +483,18 @@ export class McpClientManager {
373
483
 
374
484
  for (const [name, server] of this.servers.entries()) {
375
485
  seen.add(name);
486
+ const isRateLimited = this._isRateLimited(name);
487
+ const cd = this.cooldowns.get(name);
376
488
  list.push({
377
489
  name,
378
- status: 'connected',
379
- type: server.config.type || (server.config.command ? 'stdio' : 'streamable-http'),
380
- toolsCount: server.tools.length,
381
- resourcesCount: server.resources.length,
382
- promptsCount: server.prompts.length,
490
+ status: isRateLimited ? 'rate_limited' : 'connected',
491
+ type: server.config?.type || (server.config?.command ? 'stdio' : 'streamable-http'),
492
+ toolsCount: server.tools?.length || 0,
493
+ resourcesCount: server.resources?.length || 0,
494
+ promptsCount: server.prompts?.length || 0,
383
495
  connectedAt: server.connectedAt,
496
+ isLazy: Boolean(server.isLazy),
497
+ cooldownRemainingSec: isRateLimited ? Math.max(1, Math.ceil((cd.until - Date.now()) / 1000)) : null,
384
498
  sources: [...(this.configSources.get(name) || [])],
385
499
  config: server.config,
386
500
  });
@@ -392,9 +506,9 @@ export class McpClientManager {
392
506
  name,
393
507
  status: cfg.lastError ? 'error' : (cfg.enabled === false ? 'disabled' : 'saved_offline'),
394
508
  type: cfg.type || (cfg.command ? 'stdio' : 'streamable-http'),
395
- toolsCount: 0,
396
- resourcesCount: 0,
397
- promptsCount: 0,
509
+ toolsCount: (cfg.tools || []).length,
510
+ resourcesCount: (cfg.resources || []).length,
511
+ promptsCount: (cfg.prompts || []).length,
398
512
  error: cfg.lastError || null,
399
513
  sources: [...(this.configSources.get(name) || [])],
400
514
  config: cfg,
@@ -455,8 +569,44 @@ export class McpClientManager {
455
569
  this._trackConfigSource(name, filePath);
456
570
  if (cfg.enabled === false || cfg.disabled === true) continue;
457
571
  if (this.servers.has(name)) continue;
572
+
573
+ // If tools are cached or it's a known Kiku remote MCP server, register lazily without network call on startup
574
+ let tools = cfg.tools;
575
+ if (!tools || tools.length === 0) {
576
+ if (name === 'kiku-mcp' || /jujingyi-kikuuu|vercel\.app/i.test(cfg.url || '')) {
577
+ tools = getPredefinedKikuMcpTools();
578
+ cfg.tools = tools;
579
+ }
580
+ }
581
+
582
+ if (tools && Array.isArray(tools) && tools.length > 0) {
583
+ const serverEntry = {
584
+ name,
585
+ config: cfg,
586
+ client: null,
587
+ transport: null,
588
+ tools,
589
+ resources: cfg.resources || [],
590
+ prompts: cfg.prompts || [],
591
+ connectedAt: new Date().toISOString(),
592
+ isLazy: true,
593
+ };
594
+ this.servers.set(name, serverEntry);
595
+ this._rebuildToolRegistry();
596
+ results.push({
597
+ success: true,
598
+ serverName: name,
599
+ toolsCount: tools.length,
600
+ resourcesCount: (cfg.resources || []).length,
601
+ promptsCount: (cfg.prompts || []).length,
602
+ tools,
603
+ lazy: true,
604
+ });
605
+ continue;
606
+ }
607
+
458
608
  try {
459
- const res = await this.connect(name, cfg, { persist: false });
609
+ const res = await this.connect(name, cfg, { persist: false, fetchMetadata: false });
460
610
  results.push(res);
461
611
  } catch (err) {
462
612
  results.push({ success: false, serverName: name, error: err.message });