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
@@ -16,27 +16,40 @@ export const worksData = [
16
16
  featured: true, highlights: ["Reunion of popular pairing Ju Jingyi + Song Weilong", "Top-tier VFX and costumes by Youku"],
17
17
  },
18
18
  {
19
- id: "wanhua-sin", titleEn: "Wanhua Sin", titleCn: "万花世界", year: 2026,
19
+ id: "wanhua-sin-upcoming", alias: "wanhua-sin", titleEn: "Wanhua Sin", titleCn: "万花世界", year: 2026,
20
20
  category: "fantasy-drama", role: "Lead Role", platform: "TBA",
21
- synopsis: "Upcoming fantasy epic. Promo OST: 'Set Fire to the Silent World' (着火的沉默世界).",
21
+ synopsis: "Upcoming fantasy epic under Ju Jingyi Studio. Promo OST: 'Set Fire to the Silent World' (着火的沉默世界).",
22
22
  themeSong: { titleEn: "Set Fire to the Silent World", titleCn: "着火的沉默世界", youtubeId: "7FXGhAmHIAo" },
23
- featured: true,
23
+ featured: true, highlights: ["Major fantasy production under Ju Jingyi Studio", "Lead promotional single 'Set Fire to the Silent World'"],
24
+ },
25
+ {
26
+ id: "lets-fight-upcoming", alias: "lets-fight", titleEn: "Let's Fight", titleCn: "来战", year: 2026,
27
+ category: "modern-drama", role: "Lead Role", platform: "Youku",
28
+ synopsis: "High-octane action fantasy drama showcasing Ju Jingyi's dynamic combat choreography and powerful screen presence.",
29
+ rating: "9.5/10 (Most Anticipated)", featured: false, highlights: ["Highly anticipated action fantasy production", "Bold new athletic choreography"],
24
30
  },
25
31
  {
26
32
  id: "in-blossom", titleEn: "In Blossom", titleCn: "花间令", year: 2024,
27
- category: "historical-drama", role: "Yang Caiwei / Shangguan Zhi (杨采薇 / 上官芷)",
33
+ category: "historical-drama", role: "Yang Caiwei in Shangguan Zhi's appearance after the face-swap premise (杨采薇 / 上官芷)",
28
34
  roleCn: "杨采薇 / 上官芷", episodes: 32, rating: "9.5/10", platform: "Youku",
29
35
  synopsis: "Romantic mystery set in ancient China. Yang Caiwei, a woman presumed dead, returns in a new identity to solve mysteries alongside the man who loved her.",
30
- featured: true, highlights: ["Popular Actress of the Year — Weibo TV & Video Summit 2024", "Fastest 10,000 popularity index on Youku in 2024", "Acclaimed dual-role performance with contrasting personalities"],
36
+ featured: true, highlights: ["Popular Actress of the Year — Weibo TV & Video Summit 2024", "Fastest 10,000 popularity index on Youku in 2024", "The performance follows Yang Caiwei's identity in Shangguan Zhi's appearance; it is not two separate characters throughout the story"],
31
37
  },
32
38
  {
33
- id: "sword-fairy-4", titleEn: "Sword and Fairy 4", titleCn: "仙剑四", year: 2024,
39
+ id: "sword-and-fairy-4", alias: "sword-fairy-4", titleEn: "Sword and Fairy 4", titleCn: "仙剑四", year: 2024,
34
40
  category: "xianxia-drama", role: "Han Lingsha (韩菱纱)",
35
41
  roleCn: "韩菱纱", episodes: 36, rating: "8.9/10", platform: "iQIYI",
36
42
  synopsis: "Adapted from the classic RPG. Han Lingsha, a spirited tomb raider, joins forces with Yun Tianhe on an adventure to save the world.",
37
43
  themeSong: { titleEn: "Ru Sha", titleCn: "如纱", youtubeId: "lzDP2MfP3eA" },
38
44
  highlights: ["Iconic red costume faithful to the game", "#1 trending on iQIYI across China and Southeast Asia"],
39
45
  },
46
+ {
47
+ id: "money-bastion-movie", titleEn: "Money Bastion", titleCn: "金钱堡垒", year: 2024,
48
+ category: "movie", role: "Han Xiaomei (韩小美)",
49
+ roleCn: "韩小美", episodes: "100 min Feature Film", rating: "8.6/10", platform: "Theatrical Release",
50
+ synopsis: "Crime psychological thriller directed by Oxide Pang. Ju Jingyi stars as Han Xiaomei alongside Wang Qianyuan and Feng Shaofeng, marking her theatrical feature film debut.",
51
+ featured: false, highlights: ["Ju Jingyi's feature theatrical cinema debut", "Collaboration with veteran acclaimed actors Wang Qianyuan and Feng Shaofeng"],
52
+ },
40
53
  {
41
54
  id: "beauty-of-resilience", titleEn: "Beauty of Resilience", titleCn: "花戎", year: 2023,
42
55
  category: "xianxia-drama", role: "Wei Zhi / Sima Wangyue / A Ya (魏枝 / 司马忘月 / 阿涯)",
@@ -54,7 +67,7 @@ export const worksData = [
54
67
  highlights: ["Most Popular Actress — Tencent Video Starlight Awards 2021", "Viral bead headdress fashion trend"],
55
68
  },
56
69
  {
57
- id: "ruyi-pavilion", titleEn: "The Blooms at Ruyi Pavilion", titleCn: "如意芳霏", year: 2020,
70
+ id: "the-blooms-at-ruyi-pavilion", alias: "ruyi-pavilion", titleEn: "The Blooms at Ruyi Pavilion", titleCn: "如意芳霏", year: 2020,
58
71
  category: "historical-drama", role: "Fu Rong (傅容)",
59
72
  roleCn: "傅容", episodes: 40, rating: "9.2/10", platform: "iQIYI",
60
73
  synopsis: "Fu Rong, the second daughter of the Fu family, has prophetic dreams. She tries to escape her predicted fate of marrying Prince Su, but finds love instead.",
@@ -66,10 +79,11 @@ export const worksData = [
66
79
  category: "historical-drama", role: "Xue Wenxi / Wen Bin (雪文曦 / 文彬)",
67
80
  roleCn: "雪文曦 / 文彬", episodes: 36, rating: "8.8/10", platform: "iQIYI",
68
81
  synopsis: "Romantic comedy. Xue Wenxi disguises herself as a man to earn a living, accidentally entering an all-male academy where she forms the 'Four Scholars of Yunshang'.",
82
+ themeSong: { titleEn: "Zhuo Guang Qu", titleCn: "琢光曲", youtubeId: "JNAbdAxiG9E" },
69
83
  highlights: ["Breakthrough Actress — iQIYI Scream Night 2020", "Iconic male-disguise look went viral across Asia"],
70
84
  },
71
85
  {
72
- id: "legend-of-white-snake", titleEn: "The Legend of White Snake", titleCn: "新白娘子传奇", year: 2019,
86
+ id: "the-legend-of-white-snake", alias: "legend-of-white-snake", titleEn: "The Legend of White Snake", titleCn: "新白娘子传奇", year: 2019,
73
87
  category: "fantasy-drama", role: "Bai Suzhen (白素贞)",
74
88
  roleCn: "白素贞", episodes: 36, rating: "9.3/10", platform: "iQIYI",
75
89
  synopsis: "Modern retelling of the classic Chinese legend. Bai Suzhen, a white snake spirit, falls in love with mortal scholar Xu Xian and opens a medical clinic.",
@@ -77,28 +91,48 @@ export const worksData = [
77
91
  highlights: ["Drama Actress of the Year — iQIYI Scream Night 2019", "Official remake of the 1992 classic, aired in 10+ countries"],
78
92
  },
79
93
  {
80
- id: "please-give-me-wings", titleEn: "Please Give Me a Pair of Wings", titleCn: "请赐我一双翅膀", year: 2019,
94
+ id: "please-give-me-a-pair-of-wings", alias: "please-give-me-wings", titleEn: "Please Give Me a Pair of Wings", titleCn: "请赐我一双翅膀", year: 2019,
81
95
  category: "modern-drama", role: "Lin Jiuge (林九歌)",
82
96
  roleCn: "林九歌", episodes: 60, platform: "iQIYI",
83
97
  synopsis: "A modern crime thriller. Lin Jiuge is wrongly imprisoned and must fight to prove her innocence while uncovering a larger conspiracy.",
98
+ themeSong: { titleEn: "A Pair of Wings", titleCn: "一双翅膀", youtubeId: "L0wwTYE5kpE" },
84
99
  },
85
100
  {
86
101
  id: "legend-of-yunxi", titleEn: "Legend of Yunxi", titleCn: "芸汐传", year: 2018,
87
102
  category: "historical-drama", role: "Han Yunxi (韩芸汐)",
88
103
  roleCn: "韩芸汐", episodes: 48, rating: "9.0/10", platform: "iQIYI",
89
104
  synopsis: "Breakthrough role. A talented female physician navigates palace intrigue and romance with Prince Long Feiye.",
105
+ themeSong: { titleEn: "Sigh of Yunxi", titleCn: "叹云兮", youtubeId: "oEgxqij58LQ" },
90
106
  featured: true, highlights: ["Breakthrough Actress — Weibo TV & Video Summit 2018", "Launched Ju Jingyi to top-tier actress status"],
91
107
  },
92
108
  {
93
- id: "the-fallen-flowers", titleEn: "The Fallen Flowers", titleCn: "热血长安", year: 2017,
94
- category: "historical-drama", role: "Shangguan Wan'er (上官婉儿)",
95
- roleCn: "上官婉儿", episodes: 48, platform: "Youku",
96
- synopsis: "Historical mystery drama set in the Tang Dynasty. Shangguan Wan'er solves cases alongside a team of investigators.",
109
+ id: "mr-swimmer", titleEn: "Mr. Swimmer", titleCn: "游泳先生", year: 2018,
110
+ category: "modern-drama", role: "Song Chacha (宋茶茶)",
111
+ roleCn: "宋茶茶", episodes: 46, rating: "8.5/10", platform: "Mango TV",
112
+ synopsis: "Modern youth sports romance drama. Song Chacha, a former rich girl who stays optimistic despite family misfortune, finds love with swimmer Bai Yongze (Mike Angelo).",
113
+ themeSong: { titleEn: "Missing You", titleCn: "想你了", youtubeId: "5F3k9b47Zio" },
114
+ highlights: ["International co-star pairing with Thai actor Mike Angelo", "Energetic modern romantic comedy role"],
115
+ },
116
+ {
117
+ id: "detective-samoyeds", titleEn: "Detective Samoyeds", titleCn: "热血长安", year: 2017,
118
+ category: "historical-drama", role: "Shangguan Zisu (上官紫苏)",
119
+ roleCn: "上官紫苏", episodes: 48, platform: "Youku",
120
+ synopsis: "Historical mystery drama set in the Tang Dynasty. Shangguan Zisu, known as the 'walking library' of Chang'an, solves cases alongside a team of investigators.",
121
+ },
122
+ {
123
+ id: "novoland-castle-in-the-sky", titleEn: "Novoland: The Castle in the Sky", titleCn: "九州·天空城", year: 2016,
124
+ category: "fantasy-drama", role: "Xue Feishuang (雪飞霜) — Princess of Winged Clan",
125
+ roleCn: "雪飞霜", episodes: 28, rating: "8.8/10", platform: "Tencent Video / Jiangsu TV",
126
+ synopsis: "Epic fantasy series. Ju Jingyi portrays Xue Feishuang, the proud and tragic Princess of the Winged Clan. Her performance and OST 'Zui Fei Shuang' earned widespread acclaim.",
127
+ themeSong: { titleEn: "Drunk Fei Shuang", titleCn: "醉飞霜", youtubeId: "VTyNNuUsKzM" },
128
+ highlights: ["Breakthrough fantasy performance as Princess of the Winged Clan", "Iconic character song '醉飞霜' became a classic staple"],
97
129
  },
98
130
  {
99
- id: "novoland-eagle-flag", titleEn: "Novoland: Eagle Flag", titleCn: "九州·天空城II", year: 2020,
100
- category: "fantasy-drama", role: "Feng Ruotong (风如澈)",
101
- roleCn: "风如澈", episodes: 34, platform: "Tencent Video",
102
- synopsis: "Fantasy sequel. Feng Ruotong, a spirited girl, discovers her destiny in a world of warring fantasy kingdoms.",
131
+ id: "love-under-the-full-moon", titleEn: "Love Under the Full Moon", titleCn: "满月之下请相爱", year: 2021,
132
+ category: "modern-drama", role: "Lei Chuxia (雷初夏)",
133
+ roleCn: "雷初夏", episodes: 24, rating: "8.7/10", platform: "iQIYI",
134
+ synopsis: "Romantic fantasy. Lei Chuxia loses her memory during a full moon phenomenon and crosses time to meet game developer Xu Xiaodong.",
135
+ themeSong: { titleEn: "Past Perfect", titleCn: "过去完成时", youtubeId: "GzmxD3P1o9Y" },
136
+ highlights: ["Sci-fi modern romance pairing with Zheng Yecheng", "Viral contemporary fashion styling"],
103
137
  },
104
138
  ];
@@ -74,14 +74,16 @@ export function getUnifiedAwards() {
74
74
  }
75
75
 
76
76
  export function getUnifiedBrands() {
77
- return brandsData.map(b => ({
77
+ const list = Array.isArray(brandsData) ? brandsData : (brandsData.brandList || []);
78
+ return list.map(b => ({
78
79
  id: b.id,
79
- brandNameEn: b.brand || b.nameEn || '',
80
- brandNameCn: b.brandCn || b.nameCn || b.brand || '',
80
+ brandNameEn: b.brandNameEn || b.brand || b.nameEn || '',
81
+ brandNameCn: b.brandNameCn || b.brandCn || b.nameCn || b.brand || '',
81
82
  title: b.title || 'Brand Ambassador',
82
83
  year: b.year,
83
84
  isGlobal: Boolean(b.isGlobal),
84
85
  category: b.category || 'Beauty / Fashion',
86
+ highlight: b.highlight || '',
85
87
  })).sort((a, b) => (b.year || 0) - (a.year || 0));
86
88
  }
87
89
 
@@ -1,6 +1,6 @@
1
1
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
2
2
  import { createMcpServer } from './server.js';
3
- import { checkRateLimit, securityMiddleware, MAX_REQUEST_BODY_SIZE } from './security.js';
3
+ import { checkRateLimit, securityMiddleware, validateApiKey, MAX_REQUEST_BODY_SIZE } from './security.js';
4
4
 
5
5
  function sendJson(res, statusCode, payload, headers = {}) {
6
6
  if (res.headersSent) return;
@@ -31,15 +31,6 @@ export async function handleMcpHttp(req, res) {
31
31
  const security = securityMiddleware(req, res);
32
32
  if (security.blocked || !validateRequestSize(req, res)) return;
33
33
 
34
- if (req.method === 'OPTIONS') {
35
- res.statusCode = 204;
36
- res.setHeader('Access-Control-Allow-Origin', '*');
37
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
38
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, X-API-Key, Accept');
39
- res.end();
40
- return;
41
- }
42
-
43
34
  if (req.method !== 'POST') {
44
35
  res.setHeader('Allow', 'GET, POST, OPTIONS');
45
36
  return sendJson(res, 405, {
@@ -53,7 +44,10 @@ export async function handleMcpHttp(req, res) {
53
44
  if (!rate.allowed) return sendJson(res, 429, { error: rate.error }, { 'Retry-After': '60' });
54
45
  res.setHeader('X-RateLimit-Remaining', String(rate.remaining));
55
46
 
56
- const server = createMcpServer();
47
+ // Privileged MCP tools (filesystem, secrets, skill management) are only reachable
48
+ // when the server has MCP_API_KEY configured AND this request carries the matching key.
49
+ const authorized = !!process.env.MCP_API_KEY && validateApiKey(req).valid;
50
+ const server = createMcpServer({ transport: 'http', authorized });
57
51
  const transport = new StreamableHTTPServerTransport({
58
52
  sessionIdGenerator: undefined,
59
53
  enableJsonResponse: true,
@@ -26,6 +26,30 @@ export const promptDefinitions = [
26
26
  description: 'Get a summary of Ju Jingyi\'s brand endorsement portfolio and commercial influence.',
27
27
  arguments: [],
28
28
  },
29
+ {
30
+ name: 'draft_fan_fiction',
31
+ description: 'Draft creative fan fiction scenarios incorporating authentic Ju Jingyi characters and lore.',
32
+ arguments: [
33
+ { name: 'character', description: 'Character name (e.g. "Yang Caiwei", "Han Yunxi", "Fu Rong")', required: true },
34
+ { name: 'theme', description: 'Story theme or mood (mystery, romance, fantasy adventure)', required: false },
35
+ { name: 'setting', description: 'Story backdrop or scenario', required: false },
36
+ ],
37
+ },
38
+ {
39
+ name: 'analyze_drama_rating',
40
+ description: 'Provide an analytical overview of a drama\'s reception, milestones, and iconic performances.',
41
+ arguments: [
42
+ { name: 'title', description: 'Drama title (e.g. "In Blossom", "Legend of Yunxi")', required: true },
43
+ ],
44
+ },
45
+ {
46
+ name: 'compare_discography_era',
47
+ description: 'Compare musical styles and evolution across Ju Jingyi\'s idol, solo, and studio eras.',
48
+ arguments: [
49
+ { name: 'era_a', description: 'First era (e.g. "snh48", "solo_early", "studio_era")', required: false },
50
+ { name: 'era_b', description: 'Second era (e.g. "modern", "solo")', required: false },
51
+ ],
52
+ },
29
53
  ];
30
54
 
31
55
  function recommendSeries({ mood, genre } = {}) {
@@ -127,6 +151,64 @@ function brandPortfolio() {
127
151
  return { messages: [{ role: 'user', content: { type: 'text', text } }] };
128
152
  }
129
153
 
154
+ function draftFanFiction({ character, theme, setting } = {}) {
155
+ const charName = character || 'Yang Caiwei';
156
+ const match = worksData.find(w => w.role.toLowerCase().includes(charName.toLowerCase())) || worksData[0];
157
+
158
+ const promptText = `# Ju Jingyi Fan Fiction Prompt: ${charName}
159
+
160
+ **Protagonist:** ${match.role} (from *${match.titleEn}* / ${match.titleCn})
161
+ **Setting / Context:** ${setting || 'Ancient Heyang city lantern festival market during an unsolved mystery investigation.'}
162
+ **Theme & Mood:** ${theme || 'Poetic romance, wits, resilience, and thrilling investigation'}
163
+
164
+ **Scene Starter Guidelines:**
165
+ - Maintain authentic character traits: keen intellect, quiet grace, courageous heart.
166
+ - Incorporate period-accurate sensory details: tea fragrance, silk robes, plum blossom rain.
167
+ - Explore meaningful dialogue and suspenseful plot progression.`;
168
+
169
+ return { messages: [{ role: 'user', content: { type: 'text', text: promptText } }] };
170
+ }
171
+
172
+ function analyzeDramaRating({ title } = {}) {
173
+ const q = (title || '').toLowerCase();
174
+ const match = worksData.find(w => w.titleEn.toLowerCase().includes(q) || w.titleCn.includes(q)) || worksData[0];
175
+
176
+ const analysisText = `# Drama Critical Analysis: ${match.titleEn} (${match.titleCn}, ${match.year})
177
+
178
+ - **Character Portrayed:** ${match.role}
179
+ - **Category & Platform:** ${match.category} • ${match.platform || 'iQIYI / Youku / Tencent'}
180
+ - **Critical Milestone:** ${match.featured ? 'Featured Masterpiece / Flagship Production' : 'Key Career Milestone'}
181
+ - **Synopsis Context:** ${match.synopsis || 'Comprehensive historical drama production.'}
182
+
183
+ ### Performance Analysis & Impact:
184
+ 1. **Character Arc:** Layered portrayal combining subtle emotional restraint and expressive intensity.
185
+ 2. **Costume & Visual Aesthetics:** Landmark traditional styling that generated widespread viral discussion across Asian social platforms.
186
+ 3. **Soundtrack Synergy:** Memorable OST theme integration highlighting emotional climaxes.`;
187
+
188
+ return { messages: [{ role: 'user', content: { type: 'text', text: analysisText } }] };
189
+ }
190
+
191
+ function compareDiscographyEra({ era_a, era_b } = {}) {
192
+ const eraA = era_a || 'SNH48 Idol Era (2013-2017)';
193
+ const eraB = era_b || 'Independent Studio Era (2024-2026)';
194
+
195
+ const text = `# Ju Jingyi Discography Era Comparison: ${eraA} vs ${eraB}
196
+
197
+ ## 1. SNH48 Senbatsu & Idol Foundation (2013–2017)
198
+ - **Vocal Style:** Energetic, youthful group harmonies with prominent solo violin performances (*Everyday*, *Princess's Cloak*).
199
+ - **Themes:** Star-seeking dreams, stage companionship, high-tempo pop choreography.
200
+
201
+ ## 2. Solo Breakthrough & Historical OST Era (2018–2023)
202
+ - **Vocal Style:** Breathy, emotive classical balladry with traditional Chinese instrumentation (*Don't Touch / 叹云兮*, *Fu Sheng / 浮生*).
203
+ - **Themes:** Epic drama romance, bittersweet devotion, poignant period melodies.
204
+
205
+ ## 3. Independent Studio & Modern Pop Evolution (2024–2026)
206
+ - **Vocal Style:** Polished modern production, diverse electronic/indie elements, mature artistic control.
207
+ - **Themes:** Self-discovery, artistic independence, sophisticated vocal phrasing.`;
208
+
209
+ return { messages: [{ role: 'user', content: { type: 'text', text } }] };
210
+ }
211
+
130
212
  // ── Dispatch ──
131
213
 
132
214
  const promptHandlers = {
@@ -134,6 +216,9 @@ const promptHandlers = {
134
216
  character_summary: characterSummary,
135
217
  career_overview: careerOverview,
136
218
  brand_portfolio: brandPortfolio,
219
+ draft_fan_fiction: draftFanFiction,
220
+ analyze_drama_rating: analyzeDramaRating,
221
+ compare_discography_era: compareDiscographyEra,
137
222
  };
138
223
 
139
224
  export async function getPrompt(name, args = {}) {
@@ -141,3 +226,4 @@ export async function getPrompt(name, args = {}) {
141
226
  if (!handler) throw new Error(`Unknown prompt: ${name}`);
142
227
  return handler(args);
143
228
  }
229
+
@@ -10,7 +10,7 @@ const SECURITY_CONFIG = {
10
10
  sessionTtlMs: 30 * 60_000,
11
11
  maxMessagesPerSession: 500,
12
12
 
13
- corsAllowedOrigins: (process.env.MCP_CORS_ORIGINS || '*').split(',').map(s => s.trim()),
13
+ corsAllowedOrigins: (process.env.MCP_CORS_ORIGINS || '').split(',').map(s => s.trim()).filter(Boolean),
14
14
  maxRequestBodyBytes: 1_048_576,
15
15
  };
16
16
 
@@ -21,13 +21,12 @@ export function validateApiKey(req) {
21
21
 
22
22
  let key = req.headers['x-api-key'];
23
23
  if (!key) {
24
- try {
25
- const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
26
- key = url.searchParams.get('key') || url.searchParams.get('apiKey');
27
- } catch {
24
+ const authHeader = req.headers['authorization'];
25
+ if (authHeader && typeof authHeader === 'string' && authHeader.startsWith('Bearer ')) {
26
+ key = authHeader.slice(7).trim();
28
27
  }
29
28
  }
30
- if (!key) return { valid: false, error: 'Missing API key. Set X-API-Key header or ?key= query parameter.' };
29
+ if (!key) return { valid: false, error: 'Missing API key. Set X-API-Key header or Authorization: Bearer <key>.' };
31
30
  const expected = Buffer.from(process.env.MCP_API_KEY || '');
32
31
  const received = Buffer.from(String(key));
33
32
  if (expected.length !== received.length || !timingSafeEqual(expected, received)) {
@@ -89,20 +88,24 @@ export function validateSessionId(sessionId) {
89
88
  return /^[a-zA-Z0-9_-]{1,100}$/.test(sessionId);
90
89
  }
91
90
 
92
- export function setCorsHeaders(res, origin) {
93
- const allowed = SECURITY_CONFIG.corsAllowedOrigins;
94
- const isAllowed = allowed.includes('*') || allowed.includes(origin);
95
-
96
- if (isAllowed) {
97
- res.setHeader('Access-Control-Allow-Origin', origin || '*');
98
- } else {
99
- res.setHeader('Access-Control-Allow-Origin', '');
100
- }
91
+ export function setCorsHeaders(res, origin) {
92
+ const allowed = SECURITY_CONFIG.corsAllowedOrigins;
93
+ const isLocalhost = process.env.NODE_ENV !== 'production' &&
94
+ (origin.startsWith('http://localhost:') || origin.startsWith('http://127.0.0.1:') || origin === 'http://localhost' || origin === 'http://127.0.0.1');
95
+ const isAllowed = !origin || allowed.includes(origin) || isLocalhost;
96
+
97
+ if (origin && isAllowed) {
98
+ res.setHeader('Access-Control-Allow-Origin', origin);
99
+ res.setHeader('Vary', 'Origin');
100
+ }
101
101
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
102
102
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, X-API-Key, Accept');
103
103
  res.setHeader('Access-Control-Max-Age', '86400');
104
104
  res.setHeader('X-Content-Type-Options', 'nosniff');
105
105
  res.setHeader('X-Frame-Options', 'DENY');
106
+ res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
107
+ res.setHeader('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');
108
+ res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
106
109
 
107
110
  return isAllowed;
108
111
  }
@@ -5,7 +5,12 @@ import { toolDefinitions, handleToolCall } from './tools.js';
5
5
  import { resourceDefinitions, readResource } from './resources.js';
6
6
  import { getPrompt } from './prompts.js';
7
7
 
8
- export function createMcpServer() {
8
+ export function createMcpServer(options = {}) {
9
+ const toolOptions = {
10
+ transport: options.transport || 'stdio',
11
+ authorized: options.authorized === true,
12
+ };
13
+
9
14
  const server = new McpServer({
10
15
  name: 'kiku',
11
16
  title: 'kiku',
@@ -56,7 +61,7 @@ export function createMcpServer() {
56
61
  },
57
62
  async (args) => {
58
63
  try {
59
- const result = await handleToolCall(tool.name, args);
64
+ const result = await handleToolCall(tool.name, args, toolOptions);
60
65
  return {
61
66
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
62
67
  structuredContent: result,
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { executeTool, toolsSchema } from '../agent/tools.js';
3
3
 
4
+ // Rich catalog/database tools — safe to expose over public MCP.
4
5
  const PUBLIC_TOOL_NAMES = new Set([
5
6
  'get_profile_and_fandom',
6
7
  'get_quotes',
@@ -19,9 +20,59 @@ const PUBLIC_TOOL_NAMES = new Set([
19
20
  'get_system_stats',
20
21
  ]);
21
22
 
22
- const OPTIONAL_WEB_TOOL_NAMES = new Set(['web_search', 'fetch_webpage', 'fetch_api', 'fetch_json', 'web_extract']);
23
+ // Web tools — gated behind KIKU_MCP_ENABLE_WEB_TOOLS flag.
24
+ const OPTIONAL_WEB_TOOL_NAMES = new Set(['web_search', 'fetch_webpage', 'fetch_api', 'web_extract']);
23
25
  const webToolsEnabled = process.env.KIKU_MCP_ENABLE_WEB_TOOLS === 'true';
24
26
 
27
+ // Anonymous callers may search public music metadata, but cannot mutate playback state.
28
+ const MUSIC_TOOL_NAMES = new Set([
29
+ 'search_youtube',
30
+ ]);
31
+
32
+ // Stateless engagement tools are safe to expose.
33
+ const RESEARCH_TOOL_NAMES = new Set([
34
+ 'suggest_topics',
35
+ 'run_quiz',
36
+ ]);
37
+
38
+ // Privileged tools — they touch the filesystem, server-local resources, secrets, or
39
+ // mutated server state. Exposed over MCP but REQUIRE authorization (a valid MCP_API_KEY
40
+ // / X-API-Key / Bearer header). Never callable anonymously on the public HTTP endpoint.
41
+ const PRIVILEGED_TOOL_NAMES = new Set([
42
+ 'read_local_file',
43
+ 'list_directory',
44
+ 'screenshot_webpage',
45
+ 'get_api_key',
46
+ 'manage_skill',
47
+ 'play_music',
48
+ 'stop_music',
49
+ 'mute_music',
50
+ 'unmute_music',
51
+ 'pause_music',
52
+ 'resume_music',
53
+ 'next_track',
54
+ 'prev_track',
55
+ 'add_to_queue',
56
+ 'remove_from_queue',
57
+ 'clear_queue',
58
+ 'get_queue',
59
+ 'get_current_track',
60
+ 'get_music_status',
61
+ 'run_subagent',
62
+ 'run_deep_research',
63
+ 'verify_claim',
64
+ 'fetch_json',
65
+ ]);
66
+
67
+ // Any tool that is not privileged is public.
68
+ const isPrivileged = (name) => PRIVILEGED_TOOL_NAMES.has(name);
69
+
70
+ function isAllowedTool(name) {
71
+ if (PUBLIC_TOOL_NAMES.has(name) || MUSIC_TOOL_NAMES.has(name) || RESEARCH_TOOL_NAMES.has(name)) return true;
72
+ if (OPTIONAL_WEB_TOOL_NAMES.has(name)) return webToolsEnabled;
73
+ return PRIVILEGED_TOOL_NAMES.has(name);
74
+ }
75
+
25
76
  const COMPATIBILITY_ALIASES = Object.freeze({
26
77
  get_profile: 'get_profile_and_fandom',
27
78
  get_timeline: 'get_career_timeline',
@@ -52,7 +103,7 @@ function jsonSchemaToZodShape(schema = {}) {
52
103
 
53
104
  const sourceDefinitions = toolsSchema
54
105
  .map(tool => tool.function)
55
- .filter(tool => PUBLIC_TOOL_NAMES.has(tool.name) || (webToolsEnabled && OPTIONAL_WEB_TOOL_NAMES.has(tool.name)));
106
+ .filter(tool => isAllowedTool(tool.name));
56
107
 
57
108
  const sourceByName = new Map(sourceDefinitions.map(tool => [tool.name, tool]));
58
109
 
@@ -100,6 +151,19 @@ export async function handleToolCall(toolName, args = {}, options = {}) {
100
151
  };
101
152
  }
102
153
 
154
+ const transport = options.transport || 'stdio';
155
+ if (isPrivileged(definition.targetName)) {
156
+ const authorized = options.authorized === true || transport === 'stdio';
157
+ if (!authorized) {
158
+ return {
159
+ status: 'error',
160
+ code: 'requires_authorization',
161
+ message: `The "${toolName}" tool is privileged and requires a valid MCP API key (set MCP_API_KEY and send it via X-API-Key or Authorization: Bearer).`,
162
+ availableTools: toolDefinitions.map(tool => tool.name),
163
+ };
164
+ }
165
+ }
166
+
103
167
  const adaptedArgs = { ...(args || {}) };
104
168
  if (toolName === 'search_gallery') delete adaptedArgs.query;
105
169
  if (adaptedArgs.category && definition.targetName === 'search_discography' && !adaptedArgs.type) {
@@ -0,0 +1,18 @@
1
+ import { spawn } from 'node:child_process';
2
+
3
+ export function openBrowser(url, defaultPort = 3001) {
4
+ const targetUrl = String(url || `http://localhost:${defaultPort}`);
5
+ const platform = process.platform;
6
+ try {
7
+ if (platform === 'win32') {
8
+ spawn('cmd', ['/c', 'start', '""', targetUrl], { detached: true, stdio: 'ignore' }).unref();
9
+ } else if (platform === 'darwin') {
10
+ spawn('open', [targetUrl], { detached: true, stdio: 'ignore' }).unref();
11
+ } else {
12
+ spawn('xdg-open', [targetUrl], { detached: true, stdio: 'ignore' }).unref();
13
+ }
14
+ return true;
15
+ } catch {
16
+ return false;
17
+ }
18
+ }