slackhive 0.1.37 → 0.1.39

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 (542) hide show
  1. package/.dockerignore +14 -0
  2. package/.env.example +44 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.yml +65 -0
  4. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  5. package/.github/ISSUE_TEMPLATE/feature_request.yml +38 -0
  6. package/.github/PULL_REQUEST_TEMPLATE.md +27 -0
  7. package/.github/dependabot.yml +20 -0
  8. package/.github/workflows/audit.yml +149 -0
  9. package/.github/workflows/ci.yml +135 -0
  10. package/CHANGELOG.md +52 -0
  11. package/CODE_OF_CONDUCT.md +37 -0
  12. package/CONTRIBUTING.md +204 -0
  13. package/LICENSE +21 -0
  14. package/README.md +19 -0
  15. package/SECURITY.md +47 -0
  16. package/apps/runner/Dockerfile +33 -0
  17. package/apps/runner/dist/__tests__/channel-restrictions.test.d.ts +8 -0
  18. package/apps/runner/dist/__tests__/channel-restrictions.test.js +63 -0
  19. package/apps/runner/dist/__tests__/channel-restrictions.test.js.map +1 -0
  20. package/apps/runner/dist/__tests__/claude-handler-resolve.test.d.ts +20 -0
  21. package/apps/runner/dist/__tests__/claude-handler-resolve.test.js +178 -0
  22. package/apps/runner/dist/__tests__/claude-handler-resolve.test.js.map +1 -0
  23. package/apps/runner/dist/__tests__/compile-claude-md.test.d.ts +13 -0
  24. package/apps/runner/dist/__tests__/compile-claude-md.test.js +144 -0
  25. package/apps/runner/dist/__tests__/compile-claude-md.test.js.map +1 -0
  26. package/apps/runner/dist/__tests__/memory-sync.test.d.ts +11 -0
  27. package/apps/runner/dist/__tests__/memory-sync.test.js +56 -0
  28. package/apps/runner/dist/__tests__/memory-sync.test.js.map +1 -0
  29. package/apps/runner/dist/__tests__/slack-file-support.test.d.ts +9 -0
  30. package/apps/runner/dist/__tests__/slack-file-support.test.js +271 -0
  31. package/apps/runner/dist/__tests__/slack-file-support.test.js.map +1 -0
  32. package/apps/runner/dist/__tests__/slack-formatting.test.d.ts +12 -0
  33. package/apps/runner/dist/__tests__/slack-formatting.test.js +400 -0
  34. package/apps/runner/dist/__tests__/slack-formatting.test.js.map +1 -0
  35. package/apps/runner/dist/__tests__/thread-context.test.d.ts +12 -0
  36. package/apps/runner/dist/__tests__/thread-context.test.js +182 -0
  37. package/apps/runner/dist/__tests__/thread-context.test.js.map +1 -0
  38. package/apps/runner/dist/agent-runner.d.ts +118 -0
  39. package/apps/runner/dist/agent-runner.js +352 -0
  40. package/apps/runner/dist/agent-runner.js.map +1 -0
  41. package/apps/runner/dist/claude-handler.d.ts +122 -0
  42. package/apps/runner/dist/claude-handler.js +402 -0
  43. package/apps/runner/dist/claude-handler.js.map +1 -0
  44. package/apps/runner/dist/compile-claude-md.d.ts +59 -0
  45. package/apps/runner/dist/compile-claude-md.js +291 -0
  46. package/apps/runner/dist/compile-claude-md.js.map +1 -0
  47. package/apps/runner/dist/correction-handler.d.ts +46 -0
  48. package/apps/runner/dist/correction-handler.js +162 -0
  49. package/apps/runner/dist/correction-handler.js.map +1 -0
  50. package/apps/runner/dist/correction-manager.d.ts +53 -0
  51. package/apps/runner/dist/correction-manager.js +241 -0
  52. package/apps/runner/dist/correction-manager.js.map +1 -0
  53. package/apps/runner/dist/db.d.ts +193 -0
  54. package/apps/runner/dist/db.js +492 -0
  55. package/apps/runner/dist/db.js.map +1 -0
  56. package/apps/runner/dist/index.d.ts +9 -0
  57. package/apps/runner/dist/index.js +43 -0
  58. package/apps/runner/dist/index.js.map +1 -0
  59. package/apps/runner/dist/job-scheduler.d.ts +57 -0
  60. package/apps/runner/dist/job-scheduler.js +150 -0
  61. package/apps/runner/dist/job-scheduler.js.map +1 -0
  62. package/apps/runner/dist/logger.d.ts +32 -0
  63. package/apps/runner/dist/logger.js +52 -0
  64. package/apps/runner/dist/logger.js.map +1 -0
  65. package/apps/runner/dist/mcp-process-manager.d.ts +38 -0
  66. package/apps/runner/dist/mcp-process-manager.js +189 -0
  67. package/apps/runner/dist/mcp-process-manager.js.map +1 -0
  68. package/apps/runner/dist/memory-mcp.d.ts +14 -0
  69. package/apps/runner/dist/memory-mcp.js +88 -0
  70. package/apps/runner/dist/memory-mcp.js.map +1 -0
  71. package/apps/runner/dist/memory-watcher.d.ts +78 -0
  72. package/apps/runner/dist/memory-watcher.js +220 -0
  73. package/apps/runner/dist/memory-watcher.js.map +1 -0
  74. package/apps/runner/dist/slack-handler.d.ts +120 -0
  75. package/apps/runner/dist/slack-handler.js +843 -0
  76. package/apps/runner/dist/slack-handler.js.map +1 -0
  77. package/apps/runner/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  78. package/apps/runner/package.json +42 -0
  79. package/apps/runner/src/__tests__/channel-restrictions.test.ts +75 -0
  80. package/apps/runner/src/__tests__/claude-handler-resolve.test.ts +160 -0
  81. package/apps/runner/src/__tests__/compile-claude-md.test.ts +139 -0
  82. package/apps/runner/src/__tests__/memory-sync.test.ts +59 -0
  83. package/apps/runner/src/__tests__/slack-file-support.test.ts +376 -0
  84. package/apps/runner/src/__tests__/slack-formatting.test.ts +495 -0
  85. package/apps/runner/src/__tests__/thread-context.test.ts +215 -0
  86. package/apps/runner/src/agent-runner.ts +397 -0
  87. package/apps/runner/src/claude-handler.ts +475 -0
  88. package/apps/runner/src/compile-claude-md.ts +283 -0
  89. package/apps/runner/src/correction-handler.ts +191 -0
  90. package/apps/runner/src/correction-manager.ts +285 -0
  91. package/apps/runner/src/db.ts +604 -0
  92. package/apps/runner/src/index.ts +46 -0
  93. package/apps/runner/src/job-scheduler.ts +165 -0
  94. package/apps/runner/src/logger.ts +49 -0
  95. package/apps/runner/src/mcp-process-manager.ts +195 -0
  96. package/apps/runner/src/memory-mcp.ts +85 -0
  97. package/apps/runner/src/memory-watcher.ts +215 -0
  98. package/apps/runner/src/slack-handler.ts +929 -0
  99. package/apps/runner/tsconfig.json +17 -0
  100. package/apps/runner/vitest.config.mts +17 -0
  101. package/apps/web/.eslintrc.json +3 -0
  102. package/apps/web/.next/app-build-manifest.json +323 -0
  103. package/apps/web/.next/app-path-routes-manifest.json +46 -0
  104. package/apps/web/.next/build-manifest.json +33 -0
  105. package/apps/web/.next/cache/.previewinfo +1 -0
  106. package/apps/web/.next/cache/.rscinfo +1 -0
  107. package/apps/web/.next/cache/webpack/client-production/0.pack +0 -0
  108. package/apps/web/.next/cache/webpack/client-production/1.pack +0 -0
  109. package/apps/web/.next/cache/webpack/client-production/2.pack +0 -0
  110. package/apps/web/.next/cache/webpack/client-production/3.pack +0 -0
  111. package/apps/web/.next/cache/webpack/client-production/4.pack +0 -0
  112. package/apps/web/.next/cache/webpack/client-production/index.pack +0 -0
  113. package/apps/web/.next/cache/webpack/client-production/index.pack.old +0 -0
  114. package/apps/web/.next/cache/webpack/edge-server-production/0.pack +0 -0
  115. package/apps/web/.next/cache/webpack/edge-server-production/1.pack +0 -0
  116. package/apps/web/.next/cache/webpack/edge-server-production/index.pack +0 -0
  117. package/apps/web/.next/cache/webpack/edge-server-production/index.pack.old +0 -0
  118. package/apps/web/.next/cache/webpack/server-production/0.pack +0 -0
  119. package/apps/web/.next/cache/webpack/server-production/1.pack +0 -0
  120. package/apps/web/.next/cache/webpack/server-production/2.pack +0 -0
  121. package/apps/web/.next/cache/webpack/server-production/index.pack +0 -0
  122. package/apps/web/.next/cache/webpack/server-production/index.pack.old +0 -0
  123. package/apps/web/.next/diagnostics/build-diagnostics.json +6 -0
  124. package/apps/web/.next/diagnostics/framework.json +1 -0
  125. package/apps/web/.next/package.json +1 -0
  126. package/apps/web/.next/react-loadable-manifest.json +1 -0
  127. package/apps/web/.next/server/app/_not-found/page.js +2 -0
  128. package/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -0
  129. package/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
  130. package/apps/web/.next/server/app/agents/[slug]/page.js +4 -0
  131. package/apps/web/.next/server/app/agents/[slug]/page.js.nft.json +1 -0
  132. package/apps/web/.next/server/app/agents/[slug]/page_client-reference-manifest.js +1 -0
  133. package/apps/web/.next/server/app/agents/new/page.js +2 -0
  134. package/apps/web/.next/server/app/agents/new/page.js.nft.json +1 -0
  135. package/apps/web/.next/server/app/agents/new/page_client-reference-manifest.js +1 -0
  136. package/apps/web/.next/server/app/api/agents/[id]/access/route.js +1 -0
  137. package/apps/web/.next/server/app/api/agents/[id]/access/route.js.nft.json +1 -0
  138. package/apps/web/.next/server/app/api/agents/[id]/access/route_client-reference-manifest.js +1 -0
  139. package/apps/web/.next/server/app/api/agents/[id]/claude-md/route.js +6 -0
  140. package/apps/web/.next/server/app/api/agents/[id]/claude-md/route.js.nft.json +1 -0
  141. package/apps/web/.next/server/app/api/agents/[id]/claude-md/route_client-reference-manifest.js +1 -0
  142. package/apps/web/.next/server/app/api/agents/[id]/logs/route.js +3 -0
  143. package/apps/web/.next/server/app/api/agents/[id]/logs/route.js.nft.json +1 -0
  144. package/apps/web/.next/server/app/api/agents/[id]/logs/route_client-reference-manifest.js +1 -0
  145. package/apps/web/.next/server/app/api/agents/[id]/manifest/route.js +1 -0
  146. package/apps/web/.next/server/app/api/agents/[id]/manifest/route.js.nft.json +1 -0
  147. package/apps/web/.next/server/app/api/agents/[id]/manifest/route_client-reference-manifest.js +1 -0
  148. package/apps/web/.next/server/app/api/agents/[id]/mcps/route.js +1 -0
  149. package/apps/web/.next/server/app/api/agents/[id]/mcps/route.js.nft.json +1 -0
  150. package/apps/web/.next/server/app/api/agents/[id]/mcps/route_client-reference-manifest.js +1 -0
  151. package/apps/web/.next/server/app/api/agents/[id]/memories/[memId]/route.js +1 -0
  152. package/apps/web/.next/server/app/api/agents/[id]/memories/[memId]/route.js.nft.json +1 -0
  153. package/apps/web/.next/server/app/api/agents/[id]/memories/[memId]/route_client-reference-manifest.js +1 -0
  154. package/apps/web/.next/server/app/api/agents/[id]/memories/route.js +1 -0
  155. package/apps/web/.next/server/app/api/agents/[id]/memories/route.js.nft.json +1 -0
  156. package/apps/web/.next/server/app/api/agents/[id]/memories/route_client-reference-manifest.js +1 -0
  157. package/apps/web/.next/server/app/api/agents/[id]/permissions/route.js +1 -0
  158. package/apps/web/.next/server/app/api/agents/[id]/permissions/route.js.nft.json +1 -0
  159. package/apps/web/.next/server/app/api/agents/[id]/permissions/route_client-reference-manifest.js +1 -0
  160. package/apps/web/.next/server/app/api/agents/[id]/reload/route.js +1 -0
  161. package/apps/web/.next/server/app/api/agents/[id]/reload/route.js.nft.json +1 -0
  162. package/apps/web/.next/server/app/api/agents/[id]/reload/route_client-reference-manifest.js +1 -0
  163. package/apps/web/.next/server/app/api/agents/[id]/restrictions/route.js +1 -0
  164. package/apps/web/.next/server/app/api/agents/[id]/restrictions/route.js.nft.json +1 -0
  165. package/apps/web/.next/server/app/api/agents/[id]/restrictions/route_client-reference-manifest.js +1 -0
  166. package/apps/web/.next/server/app/api/agents/[id]/route.js +33 -0
  167. package/apps/web/.next/server/app/api/agents/[id]/route.js.nft.json +1 -0
  168. package/apps/web/.next/server/app/api/agents/[id]/route_client-reference-manifest.js +1 -0
  169. package/apps/web/.next/server/app/api/agents/[id]/skills/[skillId]/route.js +1 -0
  170. package/apps/web/.next/server/app/api/agents/[id]/skills/[skillId]/route.js.nft.json +1 -0
  171. package/apps/web/.next/server/app/api/agents/[id]/skills/[skillId]/route_client-reference-manifest.js +1 -0
  172. package/apps/web/.next/server/app/api/agents/[id]/skills/route.js +1 -0
  173. package/apps/web/.next/server/app/api/agents/[id]/skills/route.js.nft.json +1 -0
  174. package/apps/web/.next/server/app/api/agents/[id]/skills/route_client-reference-manifest.js +1 -0
  175. package/apps/web/.next/server/app/api/agents/[id]/slack-info/route.js +1 -0
  176. package/apps/web/.next/server/app/api/agents/[id]/slack-info/route.js.nft.json +1 -0
  177. package/apps/web/.next/server/app/api/agents/[id]/slack-info/route_client-reference-manifest.js +1 -0
  178. package/apps/web/.next/server/app/api/agents/[id]/snapshots/[sid]/restore/route.js +1 -0
  179. package/apps/web/.next/server/app/api/agents/[id]/snapshots/[sid]/restore/route.js.nft.json +1 -0
  180. package/apps/web/.next/server/app/api/agents/[id]/snapshots/[sid]/restore/route_client-reference-manifest.js +1 -0
  181. package/apps/web/.next/server/app/api/agents/[id]/snapshots/[sid]/route.js +1 -0
  182. package/apps/web/.next/server/app/api/agents/[id]/snapshots/[sid]/route.js.nft.json +1 -0
  183. package/apps/web/.next/server/app/api/agents/[id]/snapshots/[sid]/route_client-reference-manifest.js +1 -0
  184. package/apps/web/.next/server/app/api/agents/[id]/snapshots/route.js +1 -0
  185. package/apps/web/.next/server/app/api/agents/[id]/snapshots/route.js.nft.json +1 -0
  186. package/apps/web/.next/server/app/api/agents/[id]/snapshots/route_client-reference-manifest.js +1 -0
  187. package/apps/web/.next/server/app/api/agents/[id]/start/route.js +1 -0
  188. package/apps/web/.next/server/app/api/agents/[id]/start/route.js.nft.json +1 -0
  189. package/apps/web/.next/server/app/api/agents/[id]/start/route_client-reference-manifest.js +1 -0
  190. package/apps/web/.next/server/app/api/agents/[id]/stop/route.js +1 -0
  191. package/apps/web/.next/server/app/api/agents/[id]/stop/route.js.nft.json +1 -0
  192. package/apps/web/.next/server/app/api/agents/[id]/stop/route_client-reference-manifest.js +1 -0
  193. package/apps/web/.next/server/app/api/agents/route.js +91 -0
  194. package/apps/web/.next/server/app/api/agents/route.js.nft.json +1 -0
  195. package/apps/web/.next/server/app/api/agents/route_client-reference-manifest.js +1 -0
  196. package/apps/web/.next/server/app/api/auth/login/route.js +1 -0
  197. package/apps/web/.next/server/app/api/auth/login/route.js.nft.json +1 -0
  198. package/apps/web/.next/server/app/api/auth/login/route_client-reference-manifest.js +1 -0
  199. package/apps/web/.next/server/app/api/auth/logout/route.js +1 -0
  200. package/apps/web/.next/server/app/api/auth/logout/route.js.nft.json +1 -0
  201. package/apps/web/.next/server/app/api/auth/logout/route_client-reference-manifest.js +1 -0
  202. package/apps/web/.next/server/app/api/auth/me/route.js +1 -0
  203. package/apps/web/.next/server/app/api/auth/me/route.js.nft.json +1 -0
  204. package/apps/web/.next/server/app/api/auth/me/route_client-reference-manifest.js +1 -0
  205. package/apps/web/.next/server/app/api/auth/users/[id]/route.js +1 -0
  206. package/apps/web/.next/server/app/api/auth/users/[id]/route.js.nft.json +1 -0
  207. package/apps/web/.next/server/app/api/auth/users/[id]/route_client-reference-manifest.js +1 -0
  208. package/apps/web/.next/server/app/api/auth/users/route.js +1 -0
  209. package/apps/web/.next/server/app/api/auth/users/route.js.nft.json +1 -0
  210. package/apps/web/.next/server/app/api/auth/users/route_client-reference-manifest.js +1 -0
  211. package/apps/web/.next/server/app/api/env-vars/[key]/route.js +1 -0
  212. package/apps/web/.next/server/app/api/env-vars/[key]/route.js.nft.json +1 -0
  213. package/apps/web/.next/server/app/api/env-vars/[key]/route_client-reference-manifest.js +1 -0
  214. package/apps/web/.next/server/app/api/env-vars/route.js +1 -0
  215. package/apps/web/.next/server/app/api/env-vars/route.js.nft.json +1 -0
  216. package/apps/web/.next/server/app/api/env-vars/route_client-reference-manifest.js +1 -0
  217. package/apps/web/.next/server/app/api/jobs/[id]/route.js +1 -0
  218. package/apps/web/.next/server/app/api/jobs/[id]/route.js.nft.json +1 -0
  219. package/apps/web/.next/server/app/api/jobs/[id]/route_client-reference-manifest.js +1 -0
  220. package/apps/web/.next/server/app/api/jobs/[id]/runs/route.js +1 -0
  221. package/apps/web/.next/server/app/api/jobs/[id]/runs/route.js.nft.json +1 -0
  222. package/apps/web/.next/server/app/api/jobs/[id]/runs/route_client-reference-manifest.js +1 -0
  223. package/apps/web/.next/server/app/api/jobs/route.js +1 -0
  224. package/apps/web/.next/server/app/api/jobs/route.js.nft.json +1 -0
  225. package/apps/web/.next/server/app/api/jobs/route_client-reference-manifest.js +1 -0
  226. package/apps/web/.next/server/app/api/mcps/[id]/route.js +1 -0
  227. package/apps/web/.next/server/app/api/mcps/[id]/route.js.nft.json +1 -0
  228. package/apps/web/.next/server/app/api/mcps/[id]/route_client-reference-manifest.js +1 -0
  229. package/apps/web/.next/server/app/api/mcps/[id]/test/route.js +1 -0
  230. package/apps/web/.next/server/app/api/mcps/[id]/test/route.js.nft.json +1 -0
  231. package/apps/web/.next/server/app/api/mcps/[id]/test/route_client-reference-manifest.js +1 -0
  232. package/apps/web/.next/server/app/api/mcps/route.js +1 -0
  233. package/apps/web/.next/server/app/api/mcps/route.js.nft.json +1 -0
  234. package/apps/web/.next/server/app/api/mcps/route_client-reference-manifest.js +1 -0
  235. package/apps/web/.next/server/app/api/settings/route.js +1 -0
  236. package/apps/web/.next/server/app/api/settings/route.js.nft.json +1 -0
  237. package/apps/web/.next/server/app/api/settings/route_client-reference-manifest.js +1 -0
  238. package/apps/web/.next/server/app/icon.svg/route.js +1 -0
  239. package/apps/web/.next/server/app/icon.svg/route.js.nft.json +1 -0
  240. package/apps/web/.next/server/app/jobs/page.js +2 -0
  241. package/apps/web/.next/server/app/jobs/page.js.nft.json +1 -0
  242. package/apps/web/.next/server/app/jobs/page_client-reference-manifest.js +1 -0
  243. package/apps/web/.next/server/app/login/page.js +2 -0
  244. package/apps/web/.next/server/app/login/page.js.nft.json +1 -0
  245. package/apps/web/.next/server/app/login/page_client-reference-manifest.js +1 -0
  246. package/apps/web/.next/server/app/page.js +2 -0
  247. package/apps/web/.next/server/app/page.js.nft.json +1 -0
  248. package/apps/web/.next/server/app/page_client-reference-manifest.js +1 -0
  249. package/apps/web/.next/server/app/settings/env-vars/page.js +2 -0
  250. package/apps/web/.next/server/app/settings/env-vars/page.js.nft.json +1 -0
  251. package/apps/web/.next/server/app/settings/env-vars/page_client-reference-manifest.js +1 -0
  252. package/apps/web/.next/server/app/settings/mcps/page.js +2 -0
  253. package/apps/web/.next/server/app/settings/mcps/page.js.nft.json +1 -0
  254. package/apps/web/.next/server/app/settings/mcps/page_client-reference-manifest.js +1 -0
  255. package/apps/web/.next/server/app/settings/page.js +2 -0
  256. package/apps/web/.next/server/app/settings/page.js.nft.json +1 -0
  257. package/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -0
  258. package/apps/web/.next/server/app-paths-manifest.json +46 -0
  259. package/apps/web/.next/server/chunks/1157.js +9 -0
  260. package/apps/web/.next/server/chunks/2287.js +1 -0
  261. package/apps/web/.next/server/chunks/3444.js +1 -0
  262. package/apps/web/.next/server/chunks/383.js +6 -0
  263. package/apps/web/.next/server/chunks/4012.js +58 -0
  264. package/apps/web/.next/server/chunks/6791.js +1 -0
  265. package/apps/web/.next/server/chunks/7171.js +1 -0
  266. package/apps/web/.next/server/chunks/8819.js +22 -0
  267. package/apps/web/.next/server/edge-runtime-webpack.js +2 -0
  268. package/apps/web/.next/server/edge-runtime-webpack.js.map +1 -0
  269. package/apps/web/.next/server/interception-route-rewrite-manifest.js +1 -0
  270. package/apps/web/.next/server/middleware-build-manifest.js +1 -0
  271. package/apps/web/.next/server/middleware-manifest.json +32 -0
  272. package/apps/web/.next/server/middleware-react-loadable-manifest.js +1 -0
  273. package/apps/web/.next/server/next-font-manifest.js +1 -0
  274. package/apps/web/.next/server/next-font-manifest.json +1 -0
  275. package/apps/web/.next/server/pages/_app.js +1 -0
  276. package/apps/web/.next/server/pages/_app.js.nft.json +1 -0
  277. package/apps/web/.next/server/pages/_document.js +1 -0
  278. package/apps/web/.next/server/pages/_document.js.nft.json +1 -0
  279. package/apps/web/.next/server/pages/_error.js +19 -0
  280. package/apps/web/.next/server/pages/_error.js.nft.json +1 -0
  281. package/apps/web/.next/server/pages-manifest.json +5 -0
  282. package/apps/web/.next/server/server-reference-manifest.js +1 -0
  283. package/apps/web/.next/server/server-reference-manifest.json +1 -0
  284. package/apps/web/.next/server/src/middleware.js +14 -0
  285. package/apps/web/.next/server/src/middleware.js.map +1 -0
  286. package/apps/web/.next/server/webpack-runtime.js +1 -0
  287. package/apps/web/.next/static/chunks/18-90b700ea37b686a2.js +1 -0
  288. package/apps/web/.next/static/chunks/87c73c54-24122e7b92478d00.js +1 -0
  289. package/apps/web/.next/static/chunks/9664-af80478aa73ba424.js +1 -0
  290. package/apps/web/.next/static/chunks/app/_not-found/page-b9cee17ed89ca24a.js +1 -0
  291. package/apps/web/.next/static/chunks/app/agents/[slug]/page-18369fc3fe1a9a7b.js +1 -0
  292. package/apps/web/.next/static/chunks/app/agents/new/page-bf11cf8901c7e2cd.js +1 -0
  293. package/apps/web/.next/static/chunks/app/api/agents/[id]/access/route-07f0f73ac9839899.js +1 -0
  294. package/apps/web/.next/static/chunks/app/api/agents/[id]/claude-md/route-07f0f73ac9839899.js +1 -0
  295. package/apps/web/.next/static/chunks/app/api/agents/[id]/logs/route-07f0f73ac9839899.js +1 -0
  296. package/apps/web/.next/static/chunks/app/api/agents/[id]/manifest/route-07f0f73ac9839899.js +1 -0
  297. package/apps/web/.next/static/chunks/app/api/agents/[id]/mcps/route-07f0f73ac9839899.js +1 -0
  298. package/apps/web/.next/static/chunks/app/api/agents/[id]/memories/[memId]/route-07f0f73ac9839899.js +1 -0
  299. package/apps/web/.next/static/chunks/app/api/agents/[id]/memories/route-07f0f73ac9839899.js +1 -0
  300. package/apps/web/.next/static/chunks/app/api/agents/[id]/permissions/route-07f0f73ac9839899.js +1 -0
  301. package/apps/web/.next/static/chunks/app/api/agents/[id]/reload/route-07f0f73ac9839899.js +1 -0
  302. package/apps/web/.next/static/chunks/app/api/agents/[id]/restrictions/route-07f0f73ac9839899.js +1 -0
  303. package/apps/web/.next/static/chunks/app/api/agents/[id]/route-07f0f73ac9839899.js +1 -0
  304. package/apps/web/.next/static/chunks/app/api/agents/[id]/skills/[skillId]/route-07f0f73ac9839899.js +1 -0
  305. package/apps/web/.next/static/chunks/app/api/agents/[id]/skills/route-07f0f73ac9839899.js +1 -0
  306. package/apps/web/.next/static/chunks/app/api/agents/[id]/slack-info/route-07f0f73ac9839899.js +1 -0
  307. package/apps/web/.next/static/chunks/app/api/agents/[id]/snapshots/[sid]/restore/route-07f0f73ac9839899.js +1 -0
  308. package/apps/web/.next/static/chunks/app/api/agents/[id]/snapshots/[sid]/route-07f0f73ac9839899.js +1 -0
  309. package/apps/web/.next/static/chunks/app/api/agents/[id]/snapshots/route-07f0f73ac9839899.js +1 -0
  310. package/apps/web/.next/static/chunks/app/api/agents/[id]/start/route-07f0f73ac9839899.js +1 -0
  311. package/apps/web/.next/static/chunks/app/api/agents/[id]/stop/route-07f0f73ac9839899.js +1 -0
  312. package/apps/web/.next/static/chunks/app/api/agents/route-07f0f73ac9839899.js +1 -0
  313. package/apps/web/.next/static/chunks/app/api/auth/login/route-07f0f73ac9839899.js +1 -0
  314. package/apps/web/.next/static/chunks/app/api/auth/logout/route-07f0f73ac9839899.js +1 -0
  315. package/apps/web/.next/static/chunks/app/api/auth/me/route-07f0f73ac9839899.js +1 -0
  316. package/apps/web/.next/static/chunks/app/api/auth/users/[id]/route-07f0f73ac9839899.js +1 -0
  317. package/apps/web/.next/static/chunks/app/api/auth/users/route-07f0f73ac9839899.js +1 -0
  318. package/apps/web/.next/static/chunks/app/api/env-vars/[key]/route-07f0f73ac9839899.js +1 -0
  319. package/apps/web/.next/static/chunks/app/api/env-vars/route-07f0f73ac9839899.js +1 -0
  320. package/apps/web/.next/static/chunks/app/api/jobs/[id]/route-07f0f73ac9839899.js +1 -0
  321. package/apps/web/.next/static/chunks/app/api/jobs/[id]/runs/route-07f0f73ac9839899.js +1 -0
  322. package/apps/web/.next/static/chunks/app/api/jobs/route-07f0f73ac9839899.js +1 -0
  323. package/apps/web/.next/static/chunks/app/api/mcps/[id]/route-07f0f73ac9839899.js +1 -0
  324. package/apps/web/.next/static/chunks/app/api/mcps/[id]/test/route-07f0f73ac9839899.js +1 -0
  325. package/apps/web/.next/static/chunks/app/api/mcps/route-07f0f73ac9839899.js +1 -0
  326. package/apps/web/.next/static/chunks/app/api/settings/route-07f0f73ac9839899.js +1 -0
  327. package/apps/web/.next/static/chunks/app/jobs/page-f5aa89a47c50efd8.js +1 -0
  328. package/apps/web/.next/static/chunks/app/layout-2079f4964aa7314e.js +1 -0
  329. package/apps/web/.next/static/chunks/app/login/layout-07f0f73ac9839899.js +1 -0
  330. package/apps/web/.next/static/chunks/app/login/page-aa259283dc38e8f9.js +1 -0
  331. package/apps/web/.next/static/chunks/app/page-e83437b608104dff.js +1 -0
  332. package/apps/web/.next/static/chunks/app/settings/env-vars/page-06479dbdfb78b76b.js +1 -0
  333. package/apps/web/.next/static/chunks/app/settings/mcps/page-75650686ed6490c7.js +1 -0
  334. package/apps/web/.next/static/chunks/app/settings/page-e1e62fc41ff6cddd.js +1 -0
  335. package/apps/web/.next/static/chunks/framework-811407f832a33072.js +1 -0
  336. package/apps/web/.next/static/chunks/main-3f1cddbdd67b1546.js +1 -0
  337. package/apps/web/.next/static/chunks/main-app-cebd8a6a5ccbf72d.js +1 -0
  338. package/apps/web/.next/static/chunks/pages/_app-50fa07b56b2d29ac.js +1 -0
  339. package/apps/web/.next/static/chunks/pages/_error-fed8688bdd23f211.js +1 -0
  340. package/apps/web/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  341. package/apps/web/.next/static/chunks/webpack-6c05566dba553c97.js +1 -0
  342. package/apps/web/.next/static/css/15371687405525e2.css +5 -0
  343. package/apps/web/.next/static/ikfNbLhuw7jntn35bz0lk/_buildManifest.js +1 -0
  344. package/apps/web/.next/static/ikfNbLhuw7jntn35bz0lk/_ssgManifest.js +1 -0
  345. package/apps/web/.next/trace +5 -0
  346. package/apps/web/.next/types/app/agents/[slug]/page.ts +84 -0
  347. package/apps/web/.next/types/app/agents/new/page.ts +84 -0
  348. package/apps/web/.next/types/app/api/agents/[id]/access/route.ts +347 -0
  349. package/apps/web/.next/types/app/api/agents/[id]/claude-md/route.ts +347 -0
  350. package/apps/web/.next/types/app/api/agents/[id]/logs/route.ts +347 -0
  351. package/apps/web/.next/types/app/api/agents/[id]/manifest/route.ts +347 -0
  352. package/apps/web/.next/types/app/api/agents/[id]/mcps/route.ts +347 -0
  353. package/apps/web/.next/types/app/api/agents/[id]/memories/[memId]/route.ts +347 -0
  354. package/apps/web/.next/types/app/api/agents/[id]/memories/route.ts +347 -0
  355. package/apps/web/.next/types/app/api/agents/[id]/permissions/route.ts +347 -0
  356. package/apps/web/.next/types/app/api/agents/[id]/reload/route.ts +347 -0
  357. package/apps/web/.next/types/app/api/agents/[id]/restrictions/route.ts +347 -0
  358. package/apps/web/.next/types/app/api/agents/[id]/route.ts +347 -0
  359. package/apps/web/.next/types/app/api/agents/[id]/skills/[skillId]/route.ts +347 -0
  360. package/apps/web/.next/types/app/api/agents/[id]/skills/route.ts +347 -0
  361. package/apps/web/.next/types/app/api/agents/[id]/slack-info/route.ts +347 -0
  362. package/apps/web/.next/types/app/api/agents/[id]/snapshots/[sid]/restore/route.ts +347 -0
  363. package/apps/web/.next/types/app/api/agents/[id]/snapshots/[sid]/route.ts +347 -0
  364. package/apps/web/.next/types/app/api/agents/[id]/snapshots/route.ts +347 -0
  365. package/apps/web/.next/types/app/api/agents/[id]/start/route.ts +347 -0
  366. package/apps/web/.next/types/app/api/agents/[id]/stop/route.ts +347 -0
  367. package/apps/web/.next/types/app/api/agents/route.ts +347 -0
  368. package/apps/web/.next/types/app/api/auth/login/route.ts +347 -0
  369. package/apps/web/.next/types/app/api/auth/logout/route.ts +347 -0
  370. package/apps/web/.next/types/app/api/auth/me/route.ts +347 -0
  371. package/apps/web/.next/types/app/api/auth/users/[id]/route.ts +347 -0
  372. package/apps/web/.next/types/app/api/auth/users/route.ts +347 -0
  373. package/apps/web/.next/types/app/api/env-vars/[key]/route.ts +347 -0
  374. package/apps/web/.next/types/app/api/env-vars/route.ts +347 -0
  375. package/apps/web/.next/types/app/api/jobs/[id]/route.ts +347 -0
  376. package/apps/web/.next/types/app/api/jobs/[id]/runs/route.ts +347 -0
  377. package/apps/web/.next/types/app/api/jobs/route.ts +347 -0
  378. package/apps/web/.next/types/app/api/mcps/[id]/route.ts +347 -0
  379. package/apps/web/.next/types/app/api/mcps/[id]/test/route.ts +347 -0
  380. package/apps/web/.next/types/app/api/mcps/route.ts +347 -0
  381. package/apps/web/.next/types/app/api/settings/route.ts +347 -0
  382. package/apps/web/.next/types/app/jobs/page.ts +84 -0
  383. package/apps/web/.next/types/app/login/layout.ts +84 -0
  384. package/apps/web/.next/types/app/login/page.ts +84 -0
  385. package/apps/web/.next/types/app/page.ts +84 -0
  386. package/apps/web/.next/types/app/settings/env-vars/page.ts +84 -0
  387. package/apps/web/.next/types/app/settings/mcps/page.ts +84 -0
  388. package/apps/web/.next/types/app/settings/page.ts +84 -0
  389. package/apps/web/.next/types/cache-life.d.ts +141 -0
  390. package/apps/web/.next/types/package.json +1 -0
  391. package/apps/web/.next/types/routes.d.ts +114 -0
  392. package/apps/web/.next/types/validator.ts +448 -0
  393. package/apps/web/Dockerfile +37 -0
  394. package/apps/web/next-env.d.ts +6 -0
  395. package/apps/web/next.config.js +6 -0
  396. package/apps/web/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  397. package/apps/web/package.json +48 -0
  398. package/apps/web/postcss.config.js +3 -0
  399. package/apps/web/public/logo.svg +17 -0
  400. package/apps/web/src/app/agents/[slug]/page.tsx +2235 -0
  401. package/apps/web/src/app/agents/new/page.tsx +1161 -0
  402. package/apps/web/src/app/api/agents/[id]/access/route.ts +76 -0
  403. package/apps/web/src/app/api/agents/[id]/claude-md/route.ts +111 -0
  404. package/apps/web/src/app/api/agents/[id]/logs/route.ts +84 -0
  405. package/apps/web/src/app/api/agents/[id]/manifest/route.ts +32 -0
  406. package/apps/web/src/app/api/agents/[id]/mcps/route.ts +73 -0
  407. package/apps/web/src/app/api/agents/[id]/memories/[memId]/route.ts +31 -0
  408. package/apps/web/src/app/api/agents/[id]/memories/route.ts +56 -0
  409. package/apps/web/src/app/api/agents/[id]/permissions/route.ts +74 -0
  410. package/apps/web/src/app/api/agents/[id]/reload/route.ts +33 -0
  411. package/apps/web/src/app/api/agents/[id]/restrictions/route.ts +85 -0
  412. package/apps/web/src/app/api/agents/[id]/route.ts +81 -0
  413. package/apps/web/src/app/api/agents/[id]/skills/[skillId]/route.ts +52 -0
  414. package/apps/web/src/app/api/agents/[id]/skills/route.ts +80 -0
  415. package/apps/web/src/app/api/agents/[id]/slack-info/route.ts +38 -0
  416. package/apps/web/src/app/api/agents/[id]/snapshots/[sid]/restore/route.ts +61 -0
  417. package/apps/web/src/app/api/agents/[id]/snapshots/[sid]/route.ts +53 -0
  418. package/apps/web/src/app/api/agents/[id]/snapshots/route.ts +84 -0
  419. package/apps/web/src/app/api/agents/[id]/start/route.ts +35 -0
  420. package/apps/web/src/app/api/agents/[id]/stop/route.ts +35 -0
  421. package/apps/web/src/app/api/agents/route.ts +99 -0
  422. package/apps/web/src/app/api/auth/login/route.ts +39 -0
  423. package/apps/web/src/app/api/auth/logout/route.ts +21 -0
  424. package/apps/web/src/app/api/auth/me/route.ts +24 -0
  425. package/apps/web/src/app/api/auth/users/[id]/route.ts +48 -0
  426. package/apps/web/src/app/api/auth/users/route.ts +63 -0
  427. package/apps/web/src/app/api/env-vars/[key]/route.ts +66 -0
  428. package/apps/web/src/app/api/env-vars/route.ts +59 -0
  429. package/apps/web/src/app/api/jobs/[id]/route.ts +51 -0
  430. package/apps/web/src/app/api/jobs/[id]/runs/route.ts +24 -0
  431. package/apps/web/src/app/api/jobs/route.ts +42 -0
  432. package/apps/web/src/app/api/mcps/[id]/route.ts +60 -0
  433. package/apps/web/src/app/api/mcps/[id]/test/route.ts +195 -0
  434. package/apps/web/src/app/api/mcps/route.ts +72 -0
  435. package/apps/web/src/app/api/settings/route.ts +42 -0
  436. package/apps/web/src/app/globals.css +124 -0
  437. package/apps/web/src/app/icon.svg +17 -0
  438. package/apps/web/src/app/jobs/page.tsx +543 -0
  439. package/apps/web/src/app/layout-shell.tsx +89 -0
  440. package/apps/web/src/app/layout.tsx +18 -0
  441. package/apps/web/src/app/login/layout.tsx +9 -0
  442. package/apps/web/src/app/login/page.tsx +150 -0
  443. package/apps/web/src/app/page.tsx +573 -0
  444. package/apps/web/src/app/settings/env-vars/page.tsx +216 -0
  445. package/apps/web/src/app/settings/mcps/page.tsx +763 -0
  446. package/apps/web/src/app/settings/page.tsx +528 -0
  447. package/apps/web/src/app/sidebar.tsx +345 -0
  448. package/apps/web/src/lib/__tests__/api-guard.test.ts +189 -0
  449. package/apps/web/src/lib/__tests__/auth.test.ts +262 -0
  450. package/apps/web/src/lib/__tests__/boss-registry.test.ts +323 -0
  451. package/apps/web/src/lib/__tests__/compile.test.ts +161 -0
  452. package/apps/web/src/lib/__tests__/db-agent-hierarchy.test.ts +136 -0
  453. package/apps/web/src/lib/__tests__/db-env-vars.test.ts +216 -0
  454. package/apps/web/src/lib/__tests__/db-restrictions.test.ts +117 -0
  455. package/apps/web/src/lib/__tests__/db.integration.test.ts +271 -0
  456. package/apps/web/src/lib/__tests__/diff.test.ts +102 -0
  457. package/apps/web/src/lib/__tests__/mcp-mask.test.ts +274 -0
  458. package/apps/web/src/lib/__tests__/skill-templates.test.ts +237 -0
  459. package/apps/web/src/lib/__tests__/slack-manifest.test.ts +105 -0
  460. package/apps/web/src/lib/api-guard.ts +68 -0
  461. package/apps/web/src/lib/auth-context.tsx +71 -0
  462. package/apps/web/src/lib/auth.ts +128 -0
  463. package/apps/web/src/lib/boss-registry.ts +90 -0
  464. package/apps/web/src/lib/compile.ts +51 -0
  465. package/apps/web/src/lib/db.ts +1196 -0
  466. package/apps/web/src/lib/diff.ts +43 -0
  467. package/apps/web/src/lib/mcp-mask.ts +91 -0
  468. package/apps/web/src/lib/portal.tsx +23 -0
  469. package/apps/web/src/lib/skill-templates.ts +148 -0
  470. package/apps/web/src/lib/slack-manifest.ts +85 -0
  471. package/apps/web/src/middleware.ts +68 -0
  472. package/apps/web/tailwind.config.js +6 -0
  473. package/apps/web/tsconfig.json +23 -0
  474. package/apps/web/vitest.config.mts +21 -0
  475. package/cli/.claude/settings.local.json +6 -0
  476. package/cli/README.md +281 -0
  477. package/cli/node_modules/.package-lock.json +427 -0
  478. package/cli/node_modules/commander/LICENSE +22 -0
  479. package/cli/node_modules/commander/Readme.md +1157 -0
  480. package/cli/node_modules/commander/esm.mjs +16 -0
  481. package/cli/node_modules/commander/index.js +24 -0
  482. package/cli/node_modules/commander/lib/argument.js +149 -0
  483. package/cli/node_modules/commander/lib/command.js +2509 -0
  484. package/cli/node_modules/commander/lib/error.js +39 -0
  485. package/cli/node_modules/commander/lib/help.js +520 -0
  486. package/cli/node_modules/commander/lib/option.js +330 -0
  487. package/cli/node_modules/commander/lib/suggestSimilar.js +101 -0
  488. package/cli/node_modules/commander/package-support.json +16 -0
  489. package/cli/node_modules/commander/package.json +84 -0
  490. package/cli/node_modules/commander/typings/esm.d.mts +3 -0
  491. package/cli/node_modules/commander/typings/index.d.ts +969 -0
  492. package/cli/package-lock.json +449 -0
  493. package/cli/package.json +44 -0
  494. package/cli/src/commands/init.ts +514 -0
  495. package/cli/src/commands/manage.ts +115 -0
  496. package/cli/src/index.ts +63 -0
  497. package/cli/tsconfig.json +14 -0
  498. package/docker-compose.yml +122 -0
  499. package/docs/agents/boss-agents.mdx +108 -0
  500. package/docs/agents/creating-agents.mdx +132 -0
  501. package/docs/agents/memory.mdx +113 -0
  502. package/docs/agents/tools.mdx +103 -0
  503. package/docs/configuration/env-vars.mdx +166 -0
  504. package/docs/configuration/mcp-servers.mdx +203 -0
  505. package/docs/configuration/slack-app.mdx +175 -0
  506. package/docs/docs.json +79 -0
  507. package/docs/favicon.svg +17 -0
  508. package/docs/features/history.mdx +60 -0
  509. package/docs/features/import-export.mdx +77 -0
  510. package/docs/features/logs.mdx +131 -0
  511. package/docs/features/multi-workspace.mdx +90 -0
  512. package/docs/features/scheduled-jobs.mdx +231 -0
  513. package/docs/features/users.mdx +92 -0
  514. package/docs/introduction.mdx +160 -0
  515. package/docs/logo/dark.svg +17 -0
  516. package/docs/logo/light.svg +17 -0
  517. package/docs/logo/wide-dark.svg +12 -0
  518. package/docs/logo/wide-light.svg +12 -0
  519. package/docs/quickstart.mdx +270 -0
  520. package/docs/self-hosting/docker.mdx +151 -0
  521. package/docs/self-hosting/production.mdx +176 -0
  522. package/package.json +20 -36
  523. package/packages/shared/dist/index.d.ts +8 -0
  524. package/packages/shared/dist/index.d.ts.map +1 -0
  525. package/packages/shared/dist/index.js +24 -0
  526. package/packages/shared/dist/index.js.map +1 -0
  527. package/packages/shared/dist/types.d.ts +584 -0
  528. package/packages/shared/dist/types.d.ts.map +1 -0
  529. package/packages/shared/dist/types.js +39 -0
  530. package/packages/shared/dist/types.js.map +1 -0
  531. package/packages/shared/package.json +15 -0
  532. package/packages/shared/src/db/schema.sql +354 -0
  533. package/packages/shared/src/index.ts +8 -0
  534. package/packages/shared/src/types.ts +683 -0
  535. package/packages/shared/tsconfig.json +17 -0
  536. package/scripts/dev.sh +45 -0
  537. /package/{dist → cli/dist}/commands/init.d.ts +0 -0
  538. /package/{dist → cli/dist}/commands/init.js +0 -0
  539. /package/{dist → cli/dist}/commands/manage.d.ts +0 -0
  540. /package/{dist → cli/dist}/commands/manage.js +0 -0
  541. /package/{dist → cli/dist}/index.d.ts +0 -0
  542. /package/{dist → cli/dist}/index.js +0 -0
@@ -0,0 +1,763 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * @fileoverview Settings → MCP Catalog page.
5
+ * Global MCP server catalog — add, edit, enable/disable, delete, test.
6
+ * Supports stdio, SSE, HTTP, and inline TypeScript transports.
7
+ *
8
+ * @module web/settings/mcps/page
9
+ */
10
+
11
+ import { useState, useEffect } from 'react';
12
+ import type { McpServer, McpServerType } from '@slackhive/shared';
13
+ import { useAuth } from '@/lib/auth-context';
14
+ import { Settings } from 'lucide-react';
15
+
16
+ // ─── Types ───────────────────────────────────────────────────────────────────
17
+
18
+ /** UI transport type — 'typescript' is sent to the API as 'stdio' with tsSource */
19
+ type UiTransportType = McpServerType | 'typescript';
20
+
21
+ interface EnvEntry {
22
+ key: string;
23
+ mode: 'value' | 'ref';
24
+ val: string; // raw value (mode=value) or env_vars key name (mode=ref)
25
+ }
26
+
27
+ /** A single HTTP header row — value is either a static string or pulled from an env var */
28
+ interface HeaderEntry {
29
+ name: string;
30
+ mode: 'value' | 'ref';
31
+ val: string; // static value (mode=value) or env_vars key name (mode=ref)
32
+ prefix: string; // prepended to env var value, e.g. "Bearer " (mode=ref only)
33
+ }
34
+
35
+ interface McpFormState {
36
+ name: string;
37
+ uiType: UiTransportType;
38
+ description: string;
39
+ enabled: boolean;
40
+ // stdio fields
41
+ command: string;
42
+ args: string;
43
+ envEntries: EnvEntry[];
44
+ // typescript field
45
+ tsSource: string;
46
+ // sse/http fields
47
+ url: string;
48
+ headerEntries: HeaderEntry[];
49
+ }
50
+
51
+ const DEFAULT_FORM: McpFormState = {
52
+ name: '', uiType: 'stdio', description: '', enabled: true,
53
+ command: '', args: '', envEntries: [],
54
+ tsSource: '// MCP server TypeScript source\n// See: https://modelcontextprotocol.io/docs\n',
55
+ url: '', headerEntries: [] as HeaderEntry[],
56
+ };
57
+
58
+ // ─── Page ────────────────────────────────────────────────────────────────────
59
+
60
+ export default function McpSettingsPage() {
61
+ const { canEdit } = useAuth();
62
+ const [servers, setServers] = useState<McpServer[]>([]);
63
+ const [loading, setLoading] = useState(true);
64
+ const [form, setForm] = useState<McpFormState>(DEFAULT_FORM);
65
+ const [saving, setSaving] = useState(false);
66
+ const [error, setError] = useState('');
67
+ const [editingId, setEditingId] = useState<string | null>(null);
68
+ const [showForm, setShowForm] = useState(false);
69
+ const [envVarKeys, setEnvVarKeys] = useState<string[]>([]);
70
+ const [testResults, setTestResults] = useState<Record<string, { ok: boolean; message?: string; error?: string } | 'testing'>>({});
71
+
72
+ useEffect(() => { load(); }, []);
73
+ useEffect(() => {
74
+ // Load available env var keys for the ref dropdown
75
+ fetch('/api/env-vars').then(r => r.json()).then((rows: { key: string }[]) => setEnvVarKeys(rows.map(r => r.key))).catch(() => {});
76
+ }, []);
77
+
78
+ const load = async () => {
79
+ setLoading(true);
80
+ try {
81
+ const r = await fetch('/api/mcps');
82
+ setServers(await r.json());
83
+ } finally { setLoading(false); }
84
+ };
85
+
86
+ // ─── Config builder ─────────────────────────────────────────────────────────
87
+
88
+ const buildConfig = (f: McpFormState): object => {
89
+ if (f.uiType === 'typescript') {
90
+ const env = entriesToEnv(f.envEntries);
91
+ const envRefs = entriesToRefs(f.envEntries);
92
+ const cfg: Record<string, unknown> = { command: 'tsx', tsSource: f.tsSource };
93
+ if (Object.keys(env).length > 0) cfg.env = env;
94
+ if (Object.keys(envRefs).length > 0) cfg.envRefs = envRefs;
95
+ return cfg;
96
+ }
97
+ if (f.uiType === 'stdio') {
98
+ const cfg: Record<string, unknown> = { command: f.command };
99
+ if (f.args.trim()) cfg.args = f.args.split(',').map(a => a.trim()).filter(Boolean);
100
+ const env = entriesToEnv(f.envEntries);
101
+ const envRefs = entriesToRefs(f.envEntries);
102
+ if (Object.keys(env).length > 0) cfg.env = env;
103
+ if (Object.keys(envRefs).length > 0) cfg.envRefs = envRefs;
104
+ return cfg;
105
+ }
106
+ // sse / http
107
+ const cfg: Record<string, unknown> = { url: f.url };
108
+ const headers: Record<string, string> = {};
109
+ const envRefs: Record<string, string> = {};
110
+ for (const h of f.headerEntries) {
111
+ if (!h.name) continue;
112
+ if (h.mode === 'value') {
113
+ headers[h.name] = h.val;
114
+ } else {
115
+ headers[h.name] = h.prefix; // e.g. "Bearer " — runner prepends this to env var value
116
+ envRefs[h.name] = h.val;
117
+ }
118
+ }
119
+ if (Object.keys(headers).length > 0) cfg.headers = headers;
120
+ if (Object.keys(envRefs).length > 0) cfg.envRefs = envRefs;
121
+ return cfg;
122
+ };
123
+
124
+ const entriesToEnv = (entries: EnvEntry[]) =>
125
+ Object.fromEntries(entries.filter(e => e.mode === 'value' && e.key).map(e => [e.key, e.val]));
126
+
127
+ const entriesToRefs = (entries: EnvEntry[]) =>
128
+ Object.fromEntries(entries.filter(e => e.mode === 'ref' && e.key && e.val).map(e => [e.key, e.val]));
129
+
130
+ // API type is always 'stdio' for typescript
131
+ const apiType = (uiType: UiTransportType): McpServerType =>
132
+ uiType === 'typescript' ? 'stdio' : uiType;
133
+
134
+ // ─── Handlers ───────────────────────────────────────────────────────────────
135
+
136
+ const handleSubmit = async (e: React.FormEvent) => {
137
+ e.preventDefault();
138
+ setSaving(true); setError('');
139
+ try {
140
+ const r = await fetch(editingId ? `/api/mcps/${editingId}` : '/api/mcps', {
141
+ method: editingId ? 'PATCH' : 'POST',
142
+ headers: { 'Content-Type': 'application/json' },
143
+ body: JSON.stringify({
144
+ name: form.name,
145
+ type: apiType(form.uiType),
146
+ description: form.description || undefined,
147
+ enabled: form.enabled,
148
+ config: buildConfig(form),
149
+ }),
150
+ });
151
+ if (!r.ok) { const b = await r.json(); throw new Error(b.error ?? 'Failed'); }
152
+ resetForm(); await load();
153
+ } catch (err) { setError((err as Error).message); }
154
+ finally { setSaving(false); }
155
+ };
156
+
157
+ const handleDelete = async (id: string) => {
158
+ if (!confirm('Remove this MCP server from the catalog?')) return;
159
+ await fetch(`/api/mcps/${id}`, { method: 'DELETE' });
160
+ load();
161
+ };
162
+
163
+ const handleToggle = async (server: McpServer) => {
164
+ await fetch(`/api/mcps/${server.id}`, {
165
+ method: 'PATCH', headers: { 'Content-Type': 'application/json' },
166
+ body: JSON.stringify({ enabled: !server.enabled }),
167
+ });
168
+ load();
169
+ };
170
+
171
+ const handleTest = async (server: McpServer) => {
172
+ setTestResults(prev => ({ ...prev, [server.id]: 'testing' }));
173
+ try {
174
+ const r = await fetch(`/api/mcps/${server.id}/test`, { method: 'POST' });
175
+ const result = await r.json() as { ok: boolean; message?: string; error?: string };
176
+ setTestResults(prev => ({ ...prev, [server.id]: result }));
177
+ } catch {
178
+ setTestResults(prev => ({ ...prev, [server.id]: { ok: false, error: 'Request failed' } }));
179
+ }
180
+ };
181
+
182
+ const handleEdit = (server: McpServer) => {
183
+ const cfg = server.config as unknown as Record<string, unknown>;
184
+ const isTs = typeof cfg.tsSource === 'string';
185
+ const envObj = (cfg.env as Record<string, string>) ?? {};
186
+ const envRefsObj = (cfg.envRefs as Record<string, string>) ?? {};
187
+
188
+ const envEntries: EnvEntry[] = [
189
+ ...Object.entries(envObj).map(([k, v]) => ({ key: k, mode: 'value' as const, val: v })),
190
+ ...Object.entries(envRefsObj).map(([k, v]) => ({ key: k, mode: 'ref' as const, val: v })),
191
+ ];
192
+
193
+ const headersObj = (cfg.headers as Record<string, string>) ?? {};
194
+ const envRefsObj2 = (cfg.envRefs as Record<string, string>) ?? {};
195
+ const headerEntries: HeaderEntry[] = Object.entries(headersObj).map(([name, rawVal]) => {
196
+ if (envRefsObj2[name] !== undefined) {
197
+ return { name, mode: 'ref' as const, val: envRefsObj2[name], prefix: rawVal };
198
+ }
199
+ return { name, mode: 'value' as const, val: rawVal, prefix: '' };
200
+ });
201
+
202
+ setForm({
203
+ name: server.name,
204
+ uiType: isTs ? 'typescript' : server.type,
205
+ description: server.description ?? '',
206
+ enabled: server.enabled,
207
+ command: (cfg.command as string) ?? '',
208
+ args: Array.isArray(cfg.args) ? (cfg.args as string[]).join(', ') : '',
209
+ envEntries,
210
+ tsSource: isTs ? (cfg.tsSource as string) : DEFAULT_FORM.tsSource,
211
+ url: (cfg.url as string) ?? '',
212
+ headerEntries,
213
+ });
214
+ setEditingId(server.id);
215
+ setShowForm(true);
216
+ };
217
+
218
+ const resetForm = () => { setForm(DEFAULT_FORM); setEditingId(null); setShowForm(false); setError(''); };
219
+
220
+ const f = (key: keyof McpFormState, val: unknown) => setForm(prev => ({ ...prev, [key]: val }));
221
+
222
+ // ─── Env entry helpers ──────────────────────────────────────────────────────
223
+
224
+ const addEnvEntry = () => setForm(prev => ({ ...prev, envEntries: [...prev.envEntries, { key: '', mode: 'value', val: '' }] }));
225
+ const removeEnvEntry = (i: number) => setForm(prev => ({ ...prev, envEntries: prev.envEntries.filter((_, idx) => idx !== i) }));
226
+ const updateEnvEntry = (i: number, patch: Partial<EnvEntry>) =>
227
+ setForm(prev => ({ ...prev, envEntries: prev.envEntries.map((e, idx) => idx === i ? { ...e, ...patch } : e) }));
228
+
229
+ const addHeaderEntry = () => setForm(prev => ({ ...prev, headerEntries: [...prev.headerEntries, { name: '', mode: 'value', val: '', prefix: '' }] }));
230
+ const removeHeaderEntry = (i: number) => setForm(prev => ({ ...prev, headerEntries: prev.headerEntries.filter((_, idx) => idx !== i) }));
231
+ const updateHeaderEntry = (i: number, patch: Partial<HeaderEntry>) =>
232
+ setForm(prev => ({ ...prev, headerEntries: prev.headerEntries.map((e, idx) => idx === i ? { ...e, ...patch } : e) }));
233
+
234
+ // ─── Render ──────────────────────────────────────────────────────────────────
235
+
236
+ return (
237
+ <div style={{ padding: '32px 36px', maxWidth: 860 }} className="fade-up">
238
+ {/* Header */}
239
+ <div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', marginBottom: 28 }}>
240
+ <div>
241
+ <div style={{ fontSize: 11.5, color: 'var(--muted)', marginBottom: 4 }}>Settings</div>
242
+ <h1 style={{ margin: 0, fontSize: 22, fontWeight: 600, letterSpacing: '-0.02em', color: 'var(--text)' }}>
243
+ MCP Catalog
244
+ </h1>
245
+ <p style={{ margin: '4px 0 0', fontSize: 13, color: 'var(--muted)' }}>
246
+ {servers.length} server{servers.length !== 1 ? 's' : ''} · available to all agents
247
+ </p>
248
+ </div>
249
+ {canEdit && !showForm && (
250
+ <button onClick={() => setShowForm(true)} style={{
251
+ display: 'inline-flex', alignItems: 'center', gap: 6,
252
+ background: 'var(--accent)', color: '#fff',
253
+ padding: '8px 16px', borderRadius: 8, border: 'none',
254
+ fontSize: 13, fontWeight: 500, cursor: 'pointer', fontFamily: 'var(--font-sans)',
255
+ transition: 'opacity 0.15s',
256
+ }}
257
+ onMouseEnter={e => (e.currentTarget.style.opacity = '0.85')}
258
+ onMouseLeave={e => (e.currentTarget.style.opacity = '1')}
259
+ >
260
+ <span style={{ fontSize: 16, lineHeight: 1, marginTop: -1 }}>+</span>
261
+ Add Server
262
+ </button>
263
+ )}
264
+ </div>
265
+
266
+ {/* Server list */}
267
+ {loading ? (
268
+ <div style={{ color: 'var(--muted)', fontSize: 13 }}>Loading…</div>
269
+ ) : servers.length === 0 && !showForm ? (
270
+ <div style={{
271
+ border: '1px dashed var(--border)', borderRadius: 12, padding: '48px',
272
+ textAlign: 'center', color: 'var(--subtle)',
273
+ }}>
274
+ <div style={{ marginBottom: 10, color: 'var(--subtle)' }}><Settings size={28} /></div>
275
+ <p style={{ margin: '0 0 4px', fontSize: 15, fontWeight: 500, color: 'var(--muted)' }}>No MCP servers yet</p>
276
+ <p style={{ margin: '0 0 16px', fontSize: 13 }}>Add servers to the catalog to enable agent tools.</p>
277
+ {canEdit && <button onClick={() => setShowForm(true)} style={{
278
+ background: 'var(--accent)', color: '#fff', border: 'none', borderRadius: 8,
279
+ padding: '8px 20px', fontSize: 13, fontWeight: 500, cursor: 'pointer',
280
+ fontFamily: 'var(--font-sans)',
281
+ }}>Add First Server</button>}
282
+ </div>
283
+ ) : (
284
+ <div style={{ border: '1px solid var(--border)', borderRadius: 12, overflow: 'hidden', marginBottom: 20 }}>
285
+ {servers.map((server, i) => (
286
+ <ServerRow
287
+ key={server.id} server={server}
288
+ isLast={i === servers.length - 1}
289
+ onEdit={() => handleEdit(server)}
290
+ onDelete={() => handleDelete(server.id)}
291
+ onToggle={() => handleToggle(server)}
292
+ onTest={() => handleTest(server)}
293
+ testResult={testResults[server.id]}
294
+ canEdit={canEdit}
295
+ />
296
+ ))}
297
+ </div>
298
+ )}
299
+
300
+ {/* Add/Edit form */}
301
+ {showForm && (
302
+ <div style={{
303
+ background: 'var(--surface)', border: '1px solid var(--border)',
304
+ borderRadius: 14, padding: '28px', marginTop: 4,
305
+ }}>
306
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20 }}>
307
+ <h2 style={{ margin: 0, fontSize: 16, fontWeight: 600, color: 'var(--text)' }}>
308
+ {editingId ? 'Edit Server' : 'Add MCP Server'}
309
+ </h2>
310
+ <button onClick={resetForm} style={{
311
+ background: 'none', border: 'none', cursor: 'pointer',
312
+ color: 'var(--muted)', fontSize: 18, fontFamily: 'var(--font-sans)',
313
+ }}>×</button>
314
+ </div>
315
+
316
+ {error && (
317
+ <div style={{
318
+ background: 'rgba(239,68,68,0.1)', border: '1px solid rgba(239,68,68,0.25)',
319
+ borderRadius: 7, padding: '9px 13px', marginBottom: 16,
320
+ fontSize: 13, color: '#f87171',
321
+ }}>{error}</div>
322
+ )}
323
+
324
+ <form onSubmit={handleSubmit}>
325
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14, marginBottom: 14 }}>
326
+ <FField label="Name *" required>
327
+ <input value={form.name} onChange={e => f('name', e.target.value)} placeholder="redshift-mcp"
328
+ required {...inputStyle()} />
329
+ <small style={{ color: 'var(--subtle)', fontSize: 11, marginTop: 3, display: 'block' }}>
330
+ Tool pattern: <code style={{ fontFamily: 'var(--font-mono)' }}>mcp__name__tool</code>
331
+ </small>
332
+ </FField>
333
+ <FField label="Transport Type">
334
+ <select value={form.uiType} onChange={e => f('uiType', e.target.value as UiTransportType)} {...inputStyle()}>
335
+ <option value="stdio">stdio — local subprocess</option>
336
+ <option value="typescript">TypeScript — inline script</option>
337
+ <option value="sse">SSE — remote Server-Sent Events</option>
338
+ <option value="http">HTTP — remote HTTP transport</option>
339
+ </select>
340
+ </FField>
341
+ </div>
342
+
343
+ <FField label="Description" style={{ marginBottom: 14 }}>
344
+ <input value={form.description} onChange={e => f('description', e.target.value)}
345
+ placeholder="What does this MCP server provide?" {...inputStyle()} />
346
+ </FField>
347
+
348
+ {/* stdio fields */}
349
+ {form.uiType === 'stdio' && (
350
+ <>
351
+ <FField label="Command *" style={{ marginBottom: 14 }}>
352
+ <input value={form.command} onChange={e => f('command', e.target.value)}
353
+ placeholder="node" required {...inputStyle('var(--font-mono)')} />
354
+ </FField>
355
+ <FField label="Arguments" hint="Comma-separated" style={{ marginBottom: 14 }}>
356
+ <input value={form.args} onChange={e => f('args', e.target.value)}
357
+ placeholder="/path/to/server.js, --port, 3000" {...inputStyle('var(--font-mono)')} />
358
+ </FField>
359
+ <EnvEntriesEditor entries={form.envEntries} envVarKeys={envVarKeys}
360
+ onAdd={addEnvEntry} onRemove={removeEnvEntry} onUpdate={updateEnvEntry} />
361
+ </>
362
+ )}
363
+
364
+ {/* TypeScript inline script */}
365
+ {form.uiType === 'typescript' && (
366
+ <>
367
+ <FField label="TypeScript Source *" style={{ marginBottom: 14 }}
368
+ hint="The runner saves this to disk and executes it with tsx. Must implement the MCP stdio protocol.">
369
+ <textarea value={form.tsSource} onChange={e => f('tsSource', e.target.value)}
370
+ rows={14} required {...inputStyle('var(--font-mono)')} />
371
+ </FField>
372
+ <EnvEntriesEditor entries={form.envEntries} envVarKeys={envVarKeys}
373
+ onAdd={addEnvEntry} onRemove={removeEnvEntry} onUpdate={updateEnvEntry} />
374
+ </>
375
+ )}
376
+
377
+ {/* SSE / HTTP fields */}
378
+ {(form.uiType === 'sse' || form.uiType === 'http') && (
379
+ <>
380
+ <FField label="URL *" style={{ marginBottom: 14 }}>
381
+ <input value={form.url} onChange={e => f('url', e.target.value)}
382
+ placeholder="https://mcp.example.com/sse" required type="url" {...inputStyle('var(--font-mono)')} />
383
+ </FField>
384
+ <HeaderEntriesEditor
385
+ entries={form.headerEntries} envVarKeys={envVarKeys}
386
+ onAdd={addHeaderEntry} onRemove={removeHeaderEntry} onUpdate={updateHeaderEntry}
387
+ />
388
+ </>
389
+ )}
390
+
391
+ <label style={{ display: 'flex', alignItems: 'center', gap: 9, cursor: 'pointer', marginBottom: 20 }}>
392
+ <input type="checkbox" checked={form.enabled} onChange={e => f('enabled', e.target.checked)}
393
+ style={{ accentColor: 'var(--accent)', width: 14, height: 14 }} />
394
+ <span style={{ fontSize: 13, color: 'var(--muted)' }}>
395
+ Enabled — available for agents to use
396
+ </span>
397
+ </label>
398
+
399
+ <div style={{ display: 'flex', gap: 10 }}>
400
+ <button type="submit" disabled={saving} style={{
401
+ background: saving ? 'var(--border)' : 'var(--accent)',
402
+ color: '#fff', border: 'none', borderRadius: 7,
403
+ padding: '9px 22px', fontSize: 13, fontWeight: 500,
404
+ cursor: saving ? 'not-allowed' : 'pointer', fontFamily: 'var(--font-sans)',
405
+ transition: 'opacity 0.15s',
406
+ }}>{saving ? 'Saving…' : editingId ? 'Update Server' : 'Add Server'}</button>
407
+ <button type="button" onClick={resetForm} style={{
408
+ background: 'transparent', color: 'var(--muted)',
409
+ border: '1px solid var(--border)', borderRadius: 7,
410
+ padding: '9px 22px', fontSize: 13, cursor: 'pointer', fontFamily: 'var(--font-sans)',
411
+ }}>Cancel</button>
412
+ </div>
413
+ </form>
414
+ </div>
415
+ )}
416
+ </div>
417
+ );
418
+ }
419
+
420
+ // ─── Env entries editor ───────────────────────────────────────────────────────
421
+
422
+ function EnvEntriesEditor({
423
+ entries, envVarKeys, onAdd, onRemove, onUpdate,
424
+ }: {
425
+ entries: EnvEntry[];
426
+ envVarKeys: string[];
427
+ onAdd: () => void;
428
+ onRemove: (i: number) => void;
429
+ onUpdate: (i: number, patch: Partial<EnvEntry>) => void;
430
+ }) {
431
+ return (
432
+ <div style={{ marginBottom: 14 }}>
433
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 6 }}>
434
+ <label style={{ fontSize: 12, fontWeight: 500, color: 'var(--muted)' }}>Environment Variables</label>
435
+ <button type="button" onClick={onAdd} style={{
436
+ background: 'none', border: '1px solid var(--border)', borderRadius: 5,
437
+ color: 'var(--muted)', fontSize: 11, padding: '2px 10px', cursor: 'pointer',
438
+ fontFamily: 'var(--font-sans)',
439
+ }}>+ Add</button>
440
+ </div>
441
+
442
+ {entries.length === 0 ? (
443
+ <p style={{ fontSize: 12, color: 'var(--subtle)', margin: 0, fontStyle: 'italic' }}>
444
+ No env vars — click + Add to inject variables into the subprocess.
445
+ </p>
446
+ ) : (
447
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
448
+ {entries.map((entry, i) => (
449
+ <div key={i} style={{ display: 'grid', gridTemplateColumns: '1fr auto 1fr auto', gap: 6, alignItems: 'center' }}>
450
+ {/* Key */}
451
+ <input
452
+ value={entry.key}
453
+ onChange={e => onUpdate(i, { key: e.target.value })}
454
+ placeholder="KEY_NAME"
455
+ style={{
456
+ background: 'var(--surface-2)', border: '1px solid var(--border)', borderRadius: 6,
457
+ padding: '6px 9px', color: 'var(--text)', fontSize: 12.5,
458
+ fontFamily: 'var(--font-mono)', outline: 'none',
459
+ }}
460
+ />
461
+ {/* Mode toggle */}
462
+ <select
463
+ value={entry.mode}
464
+ onChange={e => onUpdate(i, { mode: e.target.value as 'value' | 'ref', val: '' })}
465
+ style={{
466
+ background: 'var(--surface-2)', border: '1px solid var(--border)', borderRadius: 6,
467
+ padding: '6px 8px', color: 'var(--muted)', fontSize: 11.5,
468
+ fontFamily: 'var(--font-sans)', cursor: 'pointer', outline: 'none',
469
+ }}
470
+ >
471
+ <option value="value">Custom value</option>
472
+ <option value="ref">From env vars</option>
473
+ </select>
474
+ {/* Value or ref picker */}
475
+ {entry.mode === 'value' ? (
476
+ <input
477
+ type="password"
478
+ value={entry.val}
479
+ onChange={e => onUpdate(i, { val: e.target.value })}
480
+ placeholder={entry.val === '********' ? 'Current value hidden' : 'Enter value'}
481
+ style={{
482
+ background: 'var(--surface-2)', border: '1px solid var(--border)', borderRadius: 6,
483
+ padding: '6px 9px', color: 'var(--text)', fontSize: 12.5,
484
+ fontFamily: 'var(--font-mono)', outline: 'none',
485
+ }}
486
+ />
487
+ ) : (
488
+ <select
489
+ value={entry.val}
490
+ onChange={e => onUpdate(i, { val: e.target.value })}
491
+ style={{
492
+ background: 'var(--surface-2)', border: '1px solid var(--border)', borderRadius: 6,
493
+ padding: '6px 9px', color: entry.val ? 'var(--text)' : 'var(--subtle)', fontSize: 12.5,
494
+ fontFamily: 'var(--font-mono)', outline: 'none', cursor: 'pointer',
495
+ }}
496
+ >
497
+ <option value="">— pick env var —</option>
498
+ {envVarKeys.map(k => <option key={k} value={k}>{k}</option>)}
499
+ {envVarKeys.length === 0 && <option disabled>No env vars — add in Settings → Env Vars</option>}
500
+ </select>
501
+ )}
502
+ {/* Remove */}
503
+ <button type="button" onClick={() => onRemove(i)} style={{
504
+ background: 'none', border: 'none', color: '#ef4444', fontSize: 14,
505
+ cursor: 'pointer', padding: '4px 6px', lineHeight: 1,
506
+ }}>×</button>
507
+ </div>
508
+ ))}
509
+ </div>
510
+ )}
511
+ </div>
512
+ );
513
+ }
514
+
515
+ // ─── Header entries editor ────────────────────────────────────────────────────
516
+
517
+ function HeaderEntriesEditor({
518
+ entries, envVarKeys, onAdd, onRemove, onUpdate,
519
+ }: {
520
+ entries: HeaderEntry[];
521
+ envVarKeys: string[];
522
+ onAdd: () => void;
523
+ onRemove: (i: number) => void;
524
+ onUpdate: (i: number, patch: Partial<HeaderEntry>) => void;
525
+ }) {
526
+ return (
527
+ <div style={{ marginBottom: 14 }}>
528
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 6 }}>
529
+ <label style={{ fontSize: 12, fontWeight: 500, color: 'var(--muted)' }}>Headers</label>
530
+ <button type="button" onClick={onAdd} style={{
531
+ background: 'none', border: '1px solid var(--border)', borderRadius: 5,
532
+ color: 'var(--muted)', fontSize: 11, padding: '2px 10px', cursor: 'pointer',
533
+ fontFamily: 'var(--font-sans)',
534
+ }}>+ Add</button>
535
+ </div>
536
+
537
+ {entries.length === 0 ? (
538
+ <p style={{ fontSize: 12, color: 'var(--subtle)', margin: 0, fontStyle: 'italic' }}>
539
+ No headers — click + Add to include HTTP headers (e.g. Authorization).
540
+ </p>
541
+ ) : (
542
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
543
+ {entries.map((entry, i) => (
544
+ <div key={i} style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
545
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr auto 1fr auto', gap: 6, alignItems: 'center' }}>
546
+ {/* Header name */}
547
+ <input
548
+ value={entry.name}
549
+ onChange={e => onUpdate(i, { name: e.target.value })}
550
+ placeholder="Authorization"
551
+ style={{
552
+ background: 'var(--surface-2)', border: '1px solid var(--border)', borderRadius: 6,
553
+ padding: '6px 9px', color: 'var(--text)', fontSize: 12.5,
554
+ fontFamily: 'var(--font-mono)', outline: 'none',
555
+ }}
556
+ />
557
+ {/* Mode toggle */}
558
+ <select
559
+ value={entry.mode}
560
+ onChange={e => onUpdate(i, { mode: e.target.value as 'value' | 'ref', val: '', prefix: '' })}
561
+ style={{
562
+ background: 'var(--surface-2)', border: '1px solid var(--border)', borderRadius: 6,
563
+ padding: '6px 8px', color: 'var(--muted)', fontSize: 11.5,
564
+ fontFamily: 'var(--font-sans)', cursor: 'pointer', outline: 'none',
565
+ }}
566
+ >
567
+ <option value="value">Static value</option>
568
+ <option value="ref">From env var</option>
569
+ </select>
570
+ {/* Value or env var picker */}
571
+ {entry.mode === 'value' ? (
572
+ <input
573
+ value={entry.val}
574
+ onChange={e => onUpdate(i, { val: e.target.value })}
575
+ placeholder={entry.name.toLowerCase() === 'authorization' ? 'Bearer sk-...' : 'value'}
576
+ style={{
577
+ background: 'var(--surface-2)', border: '1px solid var(--border)', borderRadius: 6,
578
+ padding: '6px 9px', color: 'var(--text)', fontSize: 12.5,
579
+ fontFamily: 'var(--font-mono)', outline: 'none',
580
+ }}
581
+ />
582
+ ) : (
583
+ <select
584
+ value={entry.val}
585
+ onChange={e => onUpdate(i, { val: e.target.value })}
586
+ style={{
587
+ background: 'var(--surface-2)', border: '1px solid var(--border)', borderRadius: 6,
588
+ padding: '6px 9px', color: entry.val ? 'var(--text)' : 'var(--subtle)', fontSize: 12.5,
589
+ fontFamily: 'var(--font-mono)', outline: 'none', cursor: 'pointer',
590
+ }}
591
+ >
592
+ <option value="">— pick env var —</option>
593
+ {envVarKeys.map(k => <option key={k} value={k}>{k}</option>)}
594
+ {envVarKeys.length === 0 && <option disabled>No env vars — add in Settings → Env Vars</option>}
595
+ </select>
596
+ )}
597
+ {/* Remove */}
598
+ <button type="button" onClick={() => onRemove(i)} style={{
599
+ background: 'none', border: 'none', color: '#ef4444', fontSize: 14,
600
+ cursor: 'pointer', padding: '4px 6px', lineHeight: 1,
601
+ }}>×</button>
602
+ </div>
603
+ {/* Optional prefix for env var mode */}
604
+ {entry.mode === 'ref' && (
605
+ <div style={{ paddingLeft: 2, display: 'flex', alignItems: 'center', gap: 6 }}>
606
+ <span style={{ fontSize: 11, color: 'var(--subtle)', whiteSpace: 'nowrap' }}>Prefix (optional):</span>
607
+ <input
608
+ value={entry.prefix}
609
+ onChange={e => onUpdate(i, { prefix: e.target.value })}
610
+ placeholder='e.g. "Bearer "'
611
+ style={{
612
+ background: 'var(--surface-2)', border: '1px solid var(--border)', borderRadius: 6,
613
+ padding: '4px 9px', color: 'var(--text)', fontSize: 12,
614
+ fontFamily: 'var(--font-mono)', outline: 'none', width: 180,
615
+ }}
616
+ />
617
+ <span style={{ fontSize: 11, color: 'var(--subtle)' }}>+ value of env var</span>
618
+ </div>
619
+ )}
620
+ </div>
621
+ ))}
622
+ </div>
623
+ )}
624
+ </div>
625
+ );
626
+ }
627
+
628
+ // ─── Server row ───────────────────────────────────────────────────────────────
629
+
630
+ function ServerRow({
631
+ server, isLast, onEdit, onDelete, onToggle, onTest, testResult, canEdit,
632
+ }: {
633
+ server: McpServer; isLast: boolean;
634
+ onEdit: () => void; onDelete: () => void; onToggle: () => void; onTest: () => void;
635
+ testResult?: { ok: boolean; message?: string; error?: string } | 'testing';
636
+ canEdit: boolean;
637
+ }) {
638
+ const cfg = server.config as unknown as Record<string, unknown>;
639
+ const isTs = typeof cfg.tsSource === 'string';
640
+ const preview = server.type === 'stdio'
641
+ ? isTs
642
+ ? '[TypeScript inline script]'
643
+ : `${cfg.command} ${Array.isArray(cfg.args) ? (cfg.args as string[]).join(' ') : ''}`.trim()
644
+ : String(cfg.url ?? '');
645
+
646
+ const envCount = Object.keys((cfg.env as object) ?? {}).length + Object.keys((cfg.envRefs as object) ?? {}).length;
647
+
648
+ return (
649
+ <div style={{
650
+ borderBottom: isLast ? 'none' : '1px solid var(--border)',
651
+ opacity: server.enabled ? 1 : 0.55,
652
+ }}>
653
+ <div style={{
654
+ display: 'flex', alignItems: 'center', gap: 14, padding: '13px 16px',
655
+ transition: 'background 0.12s',
656
+ }}
657
+ onMouseEnter={e => (e.currentTarget as HTMLElement).style.background = 'rgba(255,255,255,0.02)'}
658
+ onMouseLeave={e => (e.currentTarget as HTMLElement).style.background = 'transparent'}
659
+ >
660
+ {/* Type badge */}
661
+ <span style={{
662
+ fontSize: 10.5, fontFamily: 'var(--font-mono)', fontWeight: 500,
663
+ background: 'var(--border)', color: 'var(--muted)',
664
+ padding: '2px 8px', borderRadius: 5, flexShrink: 0, letterSpacing: '0.02em',
665
+ }}>{isTs ? 'ts' : server.type}</span>
666
+
667
+ {/* Info */}
668
+ <div style={{ flex: 1, minWidth: 0 }}>
669
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
670
+ <span style={{ fontSize: 13.5, fontWeight: 600, color: 'var(--text)' }}>{server.name}</span>
671
+ {!server.enabled && (
672
+ <span style={{ fontSize: 10.5, color: 'var(--subtle)', background: 'var(--border)', padding: '1px 6px', borderRadius: 4 }}>
673
+ disabled
674
+ </span>
675
+ )}
676
+ {envCount > 0 && (
677
+ <span style={{ fontSize: 10.5, color: 'var(--muted)', background: 'var(--surface-2)', padding: '1px 6px', borderRadius: 4, border: '1px solid var(--border)' }}>
678
+ {envCount} env var{envCount !== 1 ? 's' : ''}
679
+ </span>
680
+ )}
681
+ </div>
682
+ {server.description && (
683
+ <p style={{ margin: '1px 0 0', fontSize: 12, color: 'var(--muted)' }}>{server.description}</p>
684
+ )}
685
+ <p style={{
686
+ margin: '2px 0 0', fontSize: 11.5, color: 'var(--subtle)',
687
+ fontFamily: 'var(--font-mono)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
688
+ }}>{preview}</p>
689
+ </div>
690
+
691
+ {/* Actions */}
692
+ <div style={{ display: 'flex', gap: 6, flexShrink: 0, alignItems: 'center' }}>
693
+ <ActionBtn onClick={onTest} color="var(--muted)" disabled={false}>
694
+ {testResult === 'testing' ? 'Testing…' : 'Test'}
695
+ </ActionBtn>
696
+ <ActionBtn onClick={onToggle} color="var(--muted)" disabled={!canEdit}>
697
+ {server.enabled ? 'Disable' : 'Enable'}
698
+ </ActionBtn>
699
+ {canEdit && <ActionBtn onClick={onEdit} color="var(--accent)">Edit</ActionBtn>}
700
+ {canEdit && <ActionBtn onClick={onDelete} color="#ef4444">Delete</ActionBtn>}
701
+ </div>
702
+ </div>
703
+
704
+ {/* Test result banner */}
705
+ {testResult && testResult !== 'testing' && (
706
+ <div style={{
707
+ margin: '0 16px 10px', padding: '8px 12px', borderRadius: 7, fontSize: 12,
708
+ background: testResult.ok ? 'rgba(5,150,105,0.08)' : 'rgba(239,68,68,0.08)',
709
+ border: `1px solid ${testResult.ok ? 'rgba(5,150,105,0.25)' : 'rgba(239,68,68,0.25)'}`,
710
+ color: testResult.ok ? '#059669' : '#ef4444',
711
+ }}>
712
+ {testResult.ok ? '✓ ' : '✗ '}{testResult.ok ? testResult.message : testResult.error}
713
+ </div>
714
+ )}
715
+ </div>
716
+ );
717
+ }
718
+
719
+ // ─── Primitives ───────────────────────────────────────────────────────────────
720
+
721
+ function FField({ label, hint, children, style: s, required: req }: {
722
+ label: string; hint?: string; children: React.ReactNode;
723
+ style?: React.CSSProperties; required?: boolean;
724
+ }) {
725
+ return (
726
+ <div style={s}>
727
+ <label style={{ display: 'block', fontSize: 12, fontWeight: 500, color: 'var(--muted)', marginBottom: 5 }}>
728
+ {label}{req && <span style={{ color: '#ef4444', marginLeft: 2 }}>*</span>}
729
+ </label>
730
+ {children}
731
+ {hint && <p style={{ margin: '3px 0 0', fontSize: 11, color: 'var(--subtle)' }}>{hint}</p>}
732
+ </div>
733
+ );
734
+ }
735
+
736
+ function inputStyle(fontFamily = 'var(--font-sans)'): React.HTMLAttributes<HTMLElement> & { style: React.CSSProperties } {
737
+ return {
738
+ style: {
739
+ width: '100%', background: 'var(--surface-2)', border: '1px solid var(--border)',
740
+ borderRadius: 7, padding: '8px 11px', color: 'var(--text)',
741
+ fontSize: 13, fontFamily, outline: 'none', resize: 'vertical' as const,
742
+ transition: 'border-color 0.15s',
743
+ } as React.CSSProperties,
744
+ onFocus: (e: React.FocusEvent<HTMLElement>) => ((e.currentTarget as HTMLElement).style.borderColor = 'var(--accent)'),
745
+ onBlur: (e: React.FocusEvent<HTMLElement>) => ((e.currentTarget as HTMLElement).style.borderColor = 'var(--border)'),
746
+ };
747
+ }
748
+
749
+ function ActionBtn({ children, onClick, color, disabled }: {
750
+ children: React.ReactNode; onClick: () => void; color?: string; disabled?: boolean;
751
+ }) {
752
+ return (
753
+ <button onClick={onClick} disabled={disabled} style={{
754
+ background: 'none', border: 'none', cursor: disabled ? 'not-allowed' : 'pointer',
755
+ opacity: disabled ? 0.4 : 1,
756
+ fontSize: 12, color: color ?? 'var(--muted)', fontFamily: 'var(--font-sans)',
757
+ padding: '3px 8px', borderRadius: 5, transition: 'background 0.12s, color 0.12s',
758
+ }}
759
+ onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = 'rgba(255,255,255,0.06)'; }}
760
+ onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = 'transparent'; }}
761
+ >{children}</button>
762
+ );
763
+ }