@samanhappy/mcphub 1.0.31 → 1.0.33

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 (311) hide show
  1. package/README.md +11 -1
  2. package/README.zh.md +12 -2
  3. package/dist/betterAuth.js +13 -5
  4. package/dist/betterAuth.js.map +1 -1
  5. package/dist/cli/commands/cache.js.map +1 -1
  6. package/dist/cli/commands/call.js +2 -2
  7. package/dist/cli/commands/call.js.map +1 -1
  8. package/dist/cli/commands/config.js.map +1 -1
  9. package/dist/cli/commands/discover.js.map +1 -1
  10. package/dist/cli/commands/export.js.map +1 -1
  11. package/dist/cli/commands/groups.js.map +1 -1
  12. package/dist/cli/commands/install.js +1 -1
  13. package/dist/cli/commands/install.js.map +1 -1
  14. package/dist/cli/commands/keys.js.map +1 -1
  15. package/dist/cli/commands/login.js.map +1 -1
  16. package/dist/cli/commands/servers.js.map +1 -1
  17. package/dist/cli/commands/tools.js +2 -2
  18. package/dist/cli/commands/tools.js.map +1 -1
  19. package/dist/cli/context.js +1 -1
  20. package/dist/cli/context.js.map +1 -1
  21. package/dist/cli/http.js +3 -1
  22. package/dist/cli/http.js.map +1 -1
  23. package/dist/clients/openapi.js +498 -76
  24. package/dist/clients/openapi.js.map +1 -1
  25. package/dist/config/DaoConfigService.js +3 -2
  26. package/dist/config/DaoConfigService.js.map +1 -1
  27. package/dist/config/configManager.js +9 -8
  28. package/dist/config/configManager.js.map +1 -1
  29. package/dist/config/index.js +54 -8
  30. package/dist/config/index.js.map +1 -1
  31. package/dist/config/jwt.js +2 -1
  32. package/dist/config/jwt.js.map +1 -1
  33. package/dist/config/migrationUtils.js +46 -45
  34. package/dist/config/migrationUtils.js.map +1 -1
  35. package/dist/constants/oauthServerDefaults.js +5 -0
  36. package/dist/constants/oauthServerDefaults.js.map +1 -1
  37. package/dist/controllers/activityController.js +6 -5
  38. package/dist/controllers/activityController.js.map +1 -1
  39. package/dist/controllers/authController.js +5 -4
  40. package/dist/controllers/authController.js.map +1 -1
  41. package/dist/controllers/bearerKeyController.js +7 -6
  42. package/dist/controllers/bearerKeyController.js.map +1 -1
  43. package/dist/controllers/betterAuthController.js +2 -1
  44. package/dist/controllers/betterAuthController.js.map +1 -1
  45. package/dist/controllers/builtinPromptController.js +13 -5
  46. package/dist/controllers/builtinPromptController.js.map +1 -1
  47. package/dist/controllers/builtinResourceController.js +14 -6
  48. package/dist/controllers/builtinResourceController.js.map +1 -1
  49. package/dist/controllers/changelogController.js +5 -2
  50. package/dist/controllers/changelogController.js.map +1 -1
  51. package/dist/controllers/cloudController.js +11 -10
  52. package/dist/controllers/cloudController.js.map +1 -1
  53. package/dist/controllers/configController.js +4 -3
  54. package/dist/controllers/configController.js.map +1 -1
  55. package/dist/controllers/contextCostController.js +3 -2
  56. package/dist/controllers/contextCostController.js.map +1 -1
  57. package/dist/controllers/groupController.js +2 -1
  58. package/dist/controllers/groupController.js.map +1 -1
  59. package/dist/controllers/healthController.js +2 -1
  60. package/dist/controllers/healthController.js.map +1 -1
  61. package/dist/controllers/hostedInternalController.js +2 -1
  62. package/dist/controllers/hostedInternalController.js.map +1 -1
  63. package/dist/controllers/logController.js +5 -4
  64. package/dist/controllers/logController.js.map +1 -1
  65. package/dist/controllers/mcpbController.js +23 -20
  66. package/dist/controllers/mcpbController.js.map +1 -1
  67. package/dist/controllers/oauthCallbackController.js +54 -27
  68. package/dist/controllers/oauthCallbackController.js.map +1 -1
  69. package/dist/controllers/oauthClientController.js +7 -6
  70. package/dist/controllers/oauthClientController.js.map +1 -1
  71. package/dist/controllers/oauthDynamicRegistrationController.js +22 -4
  72. package/dist/controllers/oauthDynamicRegistrationController.js.map +1 -1
  73. package/dist/controllers/oauthServerController.js +62 -15
  74. package/dist/controllers/oauthServerController.js.map +1 -1
  75. package/dist/controllers/openApiController.js +7 -6
  76. package/dist/controllers/openApiController.js.map +1 -1
  77. package/dist/controllers/promptController.js +2 -1
  78. package/dist/controllers/promptController.js.map +1 -1
  79. package/dist/controllers/registryController.js +4 -3
  80. package/dist/controllers/registryController.js.map +1 -1
  81. package/dist/controllers/serverController.js +154 -65
  82. package/dist/controllers/serverController.js.map +1 -1
  83. package/dist/controllers/templateController.js +3 -2
  84. package/dist/controllers/templateController.js.map +1 -1
  85. package/dist/controllers/toolController.js +2 -1
  86. package/dist/controllers/toolController.js.map +1 -1
  87. package/dist/dao/DaoFactory.js +3 -2
  88. package/dist/dao/DaoFactory.js.map +1 -1
  89. package/dist/dao/ServerDao.js.map +1 -1
  90. package/dist/dao/UserConfigDaoDbImpl.js.map +1 -1
  91. package/dist/dao/UserDao.js.map +1 -1
  92. package/dist/dao/base/JsonFileBaseDao.js +3 -2
  93. package/dist/dao/base/JsonFileBaseDao.js.map +1 -1
  94. package/dist/dao/examples.js +35 -34
  95. package/dist/dao/examples.js.map +1 -1
  96. package/dist/db/connection.js +59 -58
  97. package/dist/db/connection.js.map +1 -1
  98. package/dist/db/index.js +2 -1
  99. package/dist/db/index.js.map +1 -1
  100. package/dist/db/repositories/ServerRepository.js +3 -3
  101. package/dist/db/repositories/ServerRepository.js.map +1 -1
  102. package/dist/db/repositories/SystemConfigRepository.js +2 -1
  103. package/dist/db/repositories/SystemConfigRepository.js.map +1 -1
  104. package/dist/db/repositories/UserConfigRepository.js.map +1 -1
  105. package/dist/db/repositories/VectorEmbeddingRepository.js +6 -5
  106. package/dist/db/repositories/VectorEmbeddingRepository.js.map +1 -1
  107. package/dist/index.js +28 -19
  108. package/dist/index.js.map +1 -1
  109. package/dist/middlewares/auth.js +4 -3
  110. package/dist/middlewares/auth.js.map +1 -1
  111. package/dist/middlewares/index.js +2 -1
  112. package/dist/middlewares/index.js.map +1 -1
  113. package/dist/middlewares/userContext.js +7 -6
  114. package/dist/middlewares/userContext.js.map +1 -1
  115. package/dist/models/OAuth.js +7 -6
  116. package/dist/models/OAuth.js.map +1 -1
  117. package/dist/models/User.js +13 -12
  118. package/dist/models/User.js.map +1 -1
  119. package/dist/routes/index.js +11 -10
  120. package/dist/routes/index.js.map +1 -1
  121. package/dist/scripts/dao-demo.js +28 -27
  122. package/dist/scripts/dao-demo.js.map +1 -1
  123. package/dist/server.js +36 -30
  124. package/dist/server.js.map +1 -1
  125. package/dist/services/activityLoggingService.js +3 -2
  126. package/dist/services/activityLoggingService.js.map +1 -1
  127. package/dist/services/authorizationService.js +28 -0
  128. package/dist/services/authorizationService.js.map +1 -0
  129. package/dist/services/betterAuthConfig.js +1 -4
  130. package/dist/services/betterAuthConfig.js.map +1 -1
  131. package/dist/services/betterAuthSession.js +8 -7
  132. package/dist/services/betterAuthSession.js.map +1 -1
  133. package/dist/services/changelogService.js +5 -2
  134. package/dist/services/changelogService.js.map +1 -1
  135. package/dist/services/cimdClientService.js +195 -0
  136. package/dist/services/cimdClientService.js.map +1 -0
  137. package/dist/services/cloudService.js +10 -9
  138. package/dist/services/cloudService.js.map +1 -1
  139. package/dist/services/contextCostService.js +6 -2
  140. package/dist/services/contextCostService.js.map +1 -1
  141. package/dist/services/groupService.js +8 -7
  142. package/dist/services/groupService.js.map +1 -1
  143. package/dist/services/hostedAuthService.js +3 -2
  144. package/dist/services/hostedAuthService.js.map +1 -1
  145. package/dist/services/hostedControlPlaneClient.js.map +1 -1
  146. package/dist/services/hostedEventSubscriber.js +9 -8
  147. package/dist/services/hostedEventSubscriber.js.map +1 -1
  148. package/dist/services/hostedRuntimeCatalogNames.js +1 -3
  149. package/dist/services/hostedRuntimeCatalogNames.js.map +1 -1
  150. package/dist/services/keepAliveService.js +7 -6
  151. package/dist/services/keepAliveService.js.map +1 -1
  152. package/dist/services/marketService.js +2 -1
  153. package/dist/services/marketService.js.map +1 -1
  154. package/dist/services/mcpOAuthProvider.js +22 -21
  155. package/dist/services/mcpOAuthProvider.js.map +1 -1
  156. package/dist/services/mcpService.js +239 -130
  157. package/dist/services/mcpService.js.map +1 -1
  158. package/dist/services/oauthClientRegistration.js +30 -29
  159. package/dist/services/oauthClientRegistration.js.map +1 -1
  160. package/dist/services/oauthServerService.js +39 -18
  161. package/dist/services/oauthServerService.js.map +1 -1
  162. package/dist/services/oauthService.js +16 -15
  163. package/dist/services/oauthService.js.map +1 -1
  164. package/dist/services/oauthSettingsStore.js +4 -3
  165. package/dist/services/oauthSettingsStore.js.map +1 -1
  166. package/dist/services/openApiGeneratorService.js +1 -3
  167. package/dist/services/openApiGeneratorService.js.map +1 -1
  168. package/dist/services/openApiToolStatsService.js +37 -0
  169. package/dist/services/openApiToolStatsService.js.map +1 -0
  170. package/dist/services/registry.js +2 -1
  171. package/dist/services/registry.js.map +1 -1
  172. package/dist/services/requestContextService.js.map +1 -1
  173. package/dist/services/serverConfigPresenter.js +85 -0
  174. package/dist/services/serverConfigPresenter.js.map +1 -0
  175. package/dist/services/smartRoutingService.js +6 -5
  176. package/dist/services/smartRoutingService.js.map +1 -1
  177. package/dist/services/sseService.js +52 -51
  178. package/dist/services/sseService.js.map +1 -1
  179. package/dist/services/templateService.js +105 -23
  180. package/dist/services/templateService.js.map +1 -1
  181. package/dist/services/toolResultCompressionService.js +8 -5
  182. package/dist/services/toolResultCompressionService.js.map +1 -1
  183. package/dist/services/upstreamOAuthDisconnectService.js +9 -6
  184. package/dist/services/upstreamOAuthDisconnectService.js.map +1 -1
  185. package/dist/services/userService.js +5 -4
  186. package/dist/services/userService.js.map +1 -1
  187. package/dist/services/vectorSearchService.js +126 -112
  188. package/dist/services/vectorSearchService.js.map +1 -1
  189. package/dist/utils/cacheUtils.js +5 -4
  190. package/dist/utils/cacheUtils.js.map +1 -1
  191. package/dist/utils/corsOrigin.js +42 -0
  192. package/dist/utils/corsOrigin.js.map +1 -0
  193. package/dist/utils/dbRetry.js +2 -5
  194. package/dist/utils/dbRetry.js.map +1 -1
  195. package/dist/utils/frontendShell.js +2 -1
  196. package/dist/utils/frontendShell.js.map +1 -1
  197. package/dist/utils/i18n.js.map +1 -1
  198. package/dist/utils/logger.js +170 -0
  199. package/dist/utils/logger.js.map +1 -0
  200. package/dist/utils/migration.js +51 -50
  201. package/dist/utils/migration.js.map +1 -1
  202. package/dist/utils/oauthIssuer.js +57 -0
  203. package/dist/utils/oauthIssuer.js.map +1 -0
  204. package/dist/utils/openApiRequestBody.js +220 -0
  205. package/dist/utils/openApiRequestBody.js.map +1 -0
  206. package/dist/utils/parameterConversion.js +2 -1
  207. package/dist/utils/parameterConversion.js.map +1 -1
  208. package/dist/utils/path.js +9 -8
  209. package/dist/utils/path.js.map +1 -1
  210. package/dist/utils/processTree.js +47 -0
  211. package/dist/utils/processTree.js.map +1 -0
  212. package/dist/utils/proxyTrust.js.map +1 -1
  213. package/dist/utils/rateLimit.js +8 -0
  214. package/dist/utils/rateLimit.js.map +1 -1
  215. package/dist/utils/requireAdmin.js +15 -0
  216. package/dist/utils/requireAdmin.js.map +1 -0
  217. package/dist/utils/serverConfigPersistence.js +3 -0
  218. package/dist/utils/serverConfigPersistence.js.map +1 -1
  219. package/dist/utils/serverConfigValidation.js +13 -0
  220. package/dist/utils/serverConfigValidation.js.map +1 -0
  221. package/dist/utils/serverNameValidation.js +56 -0
  222. package/dist/utils/serverNameValidation.js.map +1 -0
  223. package/dist/utils/serverShutdown.js +3 -2
  224. package/dist/utils/serverShutdown.js.map +1 -1
  225. package/dist/utils/smartRouting.js +4 -3
  226. package/dist/utils/smartRouting.js.map +1 -1
  227. package/dist/utils/ssrf.js +23 -10
  228. package/dist/utils/ssrf.js.map +1 -1
  229. package/dist/utils/systemConfigCache.js +40 -2
  230. package/dist/utils/systemConfigCache.js.map +1 -1
  231. package/dist/utils/tokenCost.js.map +1 -1
  232. package/dist/utils/tokenTruncation.js +9 -4
  233. package/dist/utils/tokenTruncation.js.map +1 -1
  234. package/dist/utils/version.js +3 -2
  235. package/dist/utils/version.js.map +1 -1
  236. package/frontend/dist/assets/{ActivityPage-Dx53zxEM.js → ActivityPage-Du9Nh-Jy.js} +2 -2
  237. package/frontend/dist/assets/{ActivityPage-Dx53zxEM.js.map → ActivityPage-Du9Nh-Jy.js.map} +1 -1
  238. package/frontend/dist/assets/ConfirmDialog-3APts2Y9.js +2 -0
  239. package/frontend/dist/assets/ConfirmDialog-3APts2Y9.js.map +1 -0
  240. package/frontend/dist/assets/Dashboard-J4G-AZkI.js +2 -0
  241. package/frontend/dist/assets/{Dashboard-ZYVsSvOw.js.map → Dashboard-J4G-AZkI.js.map} +1 -1
  242. package/frontend/dist/assets/{DeleteDialog-DNe-zTEl.js → DeleteDialog-D9ycSRvn.js} +2 -2
  243. package/frontend/dist/assets/{DeleteDialog-DNe-zTEl.js.map → DeleteDialog-D9ycSRvn.js.map} +1 -1
  244. package/frontend/dist/assets/{EndpointCopy-ngxLPeDF.js → EndpointCopy-DFeLcU9A.js} +2 -2
  245. package/frontend/dist/assets/{EndpointCopy-ngxLPeDF.js.map → EndpointCopy-DFeLcU9A.js.map} +1 -1
  246. package/frontend/dist/assets/GroupsPage-CGMZBiXd.js +33 -0
  247. package/frontend/dist/assets/{GroupsPage-JPtupGpI.js.map → GroupsPage-CGMZBiXd.js.map} +1 -1
  248. package/frontend/dist/assets/{LoginPage-B6rU3Ugr.js → LoginPage-g5pJLBk0.js} +2 -2
  249. package/frontend/dist/assets/{LoginPage-B6rU3Ugr.js.map → LoginPage-g5pJLBk0.js.map} +1 -1
  250. package/frontend/dist/assets/{LogsPage-CXq382fb.js → LogsPage-Bkv7xhNf.js} +2 -2
  251. package/frontend/dist/assets/{LogsPage-CXq382fb.js.map → LogsPage-Bkv7xhNf.js.map} +1 -1
  252. package/frontend/dist/assets/MarketPage-WkLrckIr.js +3 -0
  253. package/frontend/dist/assets/MarketPage-WkLrckIr.js.map +1 -0
  254. package/frontend/dist/assets/{OAuthConsentPage-C-n6tv3-.js → OAuthConsentPage-CA35XoBK.js} +2 -2
  255. package/frontend/dist/assets/{OAuthConsentPage-C-n6tv3-.js.map → OAuthConsentPage-CA35XoBK.js.map} +1 -1
  256. package/frontend/dist/assets/{Pagination-DLSuQ8hw.js → Pagination-Dwko3g48.js} +2 -2
  257. package/frontend/dist/assets/{Pagination-DLSuQ8hw.js.map → Pagination-Dwko3g48.js.map} +1 -1
  258. package/frontend/dist/assets/{PromptsPage-BLpy76K4.js → PromptsPage-BuDdW69R.js} +2 -2
  259. package/frontend/dist/assets/{PromptsPage-BLpy76K4.js.map → PromptsPage-BuDdW69R.js.map} +1 -1
  260. package/frontend/dist/assets/{ResourcesPage-C5nynRqE.js → ResourcesPage-Cu-eD5e2.js} +2 -2
  261. package/frontend/dist/assets/{ResourcesPage-C5nynRqE.js.map → ResourcesPage-Cu-eD5e2.js.map} +1 -1
  262. package/frontend/dist/assets/{ServersPage-Ca5W9cPW.js → ServersPage-B1Np5x3N.js} +8 -8
  263. package/frontend/dist/assets/ServersPage-B1Np5x3N.js.map +1 -0
  264. package/frontend/dist/assets/{SettingsPage-DgFBjp_O.js → SettingsPage-BQN8-ZQh.js} +4 -4
  265. package/frontend/dist/assets/SettingsPage-BQN8-ZQh.js.map +1 -0
  266. package/frontend/dist/assets/{StatusDot-CmSkukx0.js → StatusDot-WyvgqplP.js} +2 -2
  267. package/frontend/dist/assets/{StatusDot-CmSkukx0.js.map → StatusDot-WyvgqplP.js.map} +1 -1
  268. package/frontend/dist/assets/{ToggleGroup-sG_2LQom.js → ToggleGroup-CmeedsZU.js} +2 -2
  269. package/frontend/dist/assets/{ToggleGroup-sG_2LQom.js.map → ToggleGroup-CmeedsZU.js.map} +1 -1
  270. package/frontend/dist/assets/UsersPage-DqDFrIIW.js +2 -0
  271. package/frontend/dist/assets/UsersPage-DqDFrIIW.js.map +1 -0
  272. package/frontend/dist/assets/contextCost-CilWqGWT.js +2 -0
  273. package/frontend/dist/assets/contextCost-CilWqGWT.js.map +1 -0
  274. package/frontend/dist/assets/{framework-vendor-I9Y6AgD5.js → framework-vendor-BHAQX05P.js} +3 -3
  275. package/frontend/dist/assets/{framework-vendor-I9Y6AgD5.js.map → framework-vendor-BHAQX05P.js.map} +1 -1
  276. package/frontend/dist/assets/{i18n-vendor-y9bLzIeh.js → i18n-vendor-CMcB7XUR.js} +2 -2
  277. package/frontend/dist/assets/{i18n-vendor-y9bLzIeh.js.map → i18n-vendor-CMcB7XUR.js.map} +1 -1
  278. package/frontend/dist/assets/{icons-vendor-BFl4A9fq.js → icons-vendor-HZHHWeTa.js} +2 -2
  279. package/frontend/dist/assets/{icons-vendor-BFl4A9fq.js.map → icons-vendor-HZHHWeTa.js.map} +1 -1
  280. package/frontend/dist/assets/index-BvU2-VT8.js +3 -0
  281. package/frontend/dist/assets/{index-CLugsCsy.js.map → index-BvU2-VT8.js.map} +1 -1
  282. package/frontend/dist/assets/index-DtrCVU1h.css +1 -0
  283. package/frontend/dist/assets/passwordValidation-DTLWgUy9.js +2 -0
  284. package/frontend/dist/assets/passwordValidation-DTLWgUy9.js.map +1 -0
  285. package/frontend/dist/assets/{resourceService-33rS8QGJ.js → resourceService-BFH4w47h.js} +2 -2
  286. package/frontend/dist/assets/{resourceService-33rS8QGJ.js.map → resourceService-BFH4w47h.js.map} +1 -1
  287. package/frontend/dist/assets/useCostData-CoM1SxdS.js +2 -0
  288. package/frontend/dist/assets/useCostData-CoM1SxdS.js.map +1 -0
  289. package/frontend/dist/assets/useSettingsData-DKrECrG1.js +2 -0
  290. package/frontend/dist/assets/{useSettingsData-fK2bv9oN.js.map → useSettingsData-DKrECrG1.js.map} +1 -1
  291. package/frontend/dist/assets/variableDetection-CIrYDjrq.js +16 -0
  292. package/frontend/dist/assets/variableDetection-CIrYDjrq.js.map +1 -0
  293. package/frontend/dist/index.html +5 -5
  294. package/package.json +2 -2
  295. package/frontend/dist/assets/ConfirmDialog-CYfXEuyU.js +0 -2
  296. package/frontend/dist/assets/ConfirmDialog-CYfXEuyU.js.map +0 -1
  297. package/frontend/dist/assets/Dashboard-ZYVsSvOw.js +0 -2
  298. package/frontend/dist/assets/GroupsPage-JPtupGpI.js +0 -33
  299. package/frontend/dist/assets/MarketPage-BGnQQyXK.js +0 -3
  300. package/frontend/dist/assets/MarketPage-BGnQQyXK.js.map +0 -1
  301. package/frontend/dist/assets/ServersPage-Ca5W9cPW.js.map +0 -1
  302. package/frontend/dist/assets/SettingsPage-DgFBjp_O.js.map +0 -1
  303. package/frontend/dist/assets/UsersPage-PHkFXLL8.js +0 -2
  304. package/frontend/dist/assets/UsersPage-PHkFXLL8.js.map +0 -1
  305. package/frontend/dist/assets/contextCost-D_iNNbhX.js +0 -2
  306. package/frontend/dist/assets/contextCost-D_iNNbhX.js.map +0 -1
  307. package/frontend/dist/assets/index-CGvImZ3F.css +0 -1
  308. package/frontend/dist/assets/index-CLugsCsy.js +0 -3
  309. package/frontend/dist/assets/useSettingsData-fK2bv9oN.js +0 -2
  310. package/frontend/dist/assets/variableDetection-ByJ_cnxa.js +0 -16
  311. package/frontend/dist/assets/variableDetection-ByJ_cnxa.js.map +0 -1
@@ -2,6 +2,7 @@ import os from 'os';
2
2
  import path from 'path';
3
3
  import fs from 'fs';
4
4
  import treeKill from 'tree-kill';
5
+ import { isProcessTreeKillAvailable } from '../utils/processTree.js';
5
6
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
6
7
  import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
7
8
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
@@ -16,6 +17,7 @@ import { ResilientJsonSchemaValidator } from '../utils/jsonSchemaValidator.js';
16
17
  import { getUserDao } from '../dao/index.js';
17
18
  import { expandEnvVars, replaceEnvVars, getNameSeparator } from '../config/index.js';
18
19
  import config from '../config/index.js';
20
+ import { validateServerName } from '../utils/serverNameValidation.js';
19
21
  import { getGroup } from './sseService.js';
20
22
  import { getServerConfigInGroup, normalizeGroupServers } from './groupService.js';
21
23
  import { removeServerToolEmbeddings, saveToolsAsVectorEmbeddings } from './vectorSearchService.js';
@@ -35,6 +37,7 @@ import { MCP_APPS_CAPABILITIES, filterModelVisibleTools, hasMcpAppsCapability, i
35
37
  import { supportsCacheRefresh, injectRefreshFlag, clearRunnerCache } from '../utils/cacheUtils.js';
36
38
  const servers = {};
37
39
  import { setupClientKeepAlive } from './keepAliveService.js';
40
+ import { logger } from '../utils/logger.js';
38
41
  /**
39
42
  * Check if proxychains4 is available on the system (Linux/macOS only).
40
43
  * Returns the path to proxychains4 if found, null otherwise.
@@ -77,12 +80,12 @@ const generateProxychainsConfig = (serverName, proxyConfig) => {
77
80
  if (fs.existsSync(proxyConfig.configPath)) {
78
81
  return proxyConfig.configPath;
79
82
  }
80
- console.warn(`[${serverName}] Custom proxychains config not found: ${proxyConfig.configPath}`);
83
+ logger.warn(`[${serverName}] Custom proxychains config not found: ${proxyConfig.configPath}`);
81
84
  return null;
82
85
  }
83
86
  // Validate required fields
84
87
  if (!proxyConfig.host || !proxyConfig.port) {
85
- console.warn(`[${serverName}] Proxy host and port are required for proxychains4`);
88
+ logger.warn(`[${serverName}] Proxy host and port are required for proxychains4`);
86
89
  return null;
87
90
  }
88
91
  const proxyType = proxyConfig.type || 'socks5';
@@ -114,7 +117,7 @@ ${proxyLine}
114
117
  // Write config file
115
118
  const configPath = path.join(tempDir, `${serverName.replace(/[^a-zA-Z0-9-_]/g, '_')}.conf`);
116
119
  fs.writeFileSync(configPath, configContent, 'utf-8');
117
- console.log(`[${serverName}] Generated proxychains4 config: ${configPath}`);
120
+ logger.log(`[${serverName}] Generated proxychains4 config: ${configPath}`);
118
121
  return configPath;
119
122
  };
120
123
  /**
@@ -124,9 +127,20 @@ ${proxyLine}
124
127
  const isSafeCommand = (command) => {
125
128
  // Block shell builtins that could be used for command injection
126
129
  const blockedCommands = new Set([
127
- 'sh', 'bash', 'zsh', 'fish', 'csh', 'ksh', 'tcsh',
128
- 'cmd', 'powershell', 'pwsh',
129
- 'eval', 'exec', 'source', '.',
130
+ 'sh',
131
+ 'bash',
132
+ 'zsh',
133
+ 'fish',
134
+ 'csh',
135
+ 'ksh',
136
+ 'tcsh',
137
+ 'cmd',
138
+ 'powershell',
139
+ 'pwsh',
140
+ 'eval',
141
+ 'exec',
142
+ 'source',
143
+ '.',
130
144
  ]);
131
145
  const basename = command.split('/').pop()?.split('\\').pop()?.toLowerCase() || '';
132
146
  if (blockedCommands.has(basename)) {
@@ -143,7 +157,7 @@ const isSafeCommand = (command) => {
143
157
  * Removes shell metacharacters and dangerous patterns.
144
158
  */
145
159
  const sanitizeArgs = (args) => {
146
- return args.map(arg => {
160
+ return args.map((arg) => {
147
161
  // Remove shell metacharacters that could be used for injection
148
162
  // Allow common flags (-f, --flag, -vvv) and paths
149
163
  if (/^[a-zA-Z0-9._/\\:@=+,-]+$/.test(arg)) {
@@ -151,7 +165,7 @@ const sanitizeArgs = (args) => {
151
165
  }
152
166
  // For arguments with special characters, log a warning
153
167
  // Also block redirection operators (>, <, >>) and newlines
154
- console.warn(`[proxychains] Potentially unsafe argument blocked: ${arg}`);
168
+ logger.warn(`[proxychains] Potentially unsafe argument blocked: ${arg}`);
155
169
  return arg.replace(/[;&|`$(){}[\]!><\n\r]/g, '');
156
170
  });
157
171
  };
@@ -166,30 +180,30 @@ const wrapWithProxychains = (serverName, command, args, proxyConfig) => {
166
180
  }
167
181
  // Check platform - Windows is not supported
168
182
  if (process.platform === 'win32') {
169
- console.warn(`[${serverName}] proxychains4 proxy is not supported on Windows, ignoring proxy configuration`);
183
+ logger.warn(`[${serverName}] proxychains4 proxy is not supported on Windows, ignoring proxy configuration`);
170
184
  return { command, args };
171
185
  }
172
186
  // SECURITY: Validate command is safe
173
187
  if (!isSafeCommand(command)) {
174
- console.error(`[${serverName}] Blocked unsafe command for proxychains4 wrapping: ${command}`);
188
+ logger.error(`[${serverName}] Blocked unsafe command for proxychains4 wrapping: ${command}`);
175
189
  throw new Error(`[${serverName}] Unsafe command blocked: ${command}. Shell builtins and metacharacters are not allowed.`);
176
190
  }
177
191
  // Find proxychains4 binary
178
192
  const proxychains4Path = findProxychains4();
179
193
  if (!proxychains4Path) {
180
- console.warn(`[${serverName}] proxychains4 not found on system, install it with: apt install proxychains4 (Debian/Ubuntu) or brew install proxychains-ng (macOS)`);
194
+ logger.warn(`[${serverName}] proxychains4 not found on system, install it with: apt install proxychains4 (Debian/Ubuntu) or brew install proxychains-ng (macOS)`);
181
195
  return { command, args };
182
196
  }
183
197
  // Generate or get config file
184
198
  const configPath = generateProxychainsConfig(serverName, proxyConfig);
185
199
  if (!configPath) {
186
- console.warn(`[${serverName}] Failed to setup proxychains4 configuration, skipping proxy`);
200
+ logger.warn(`[${serverName}] Failed to setup proxychains4 configuration, skipping proxy`);
187
201
  return { command, args };
188
202
  }
189
203
  // SECURITY: Sanitize arguments
190
204
  const sanitizedArgs = sanitizeArgs(args);
191
205
  // Wrap command with proxychains4
192
- console.log(`[${serverName}] Using proxychains4 proxy: ${proxyConfig.type || 'socks5'}://${proxyConfig.host}:${proxyConfig.port}`);
206
+ logger.log(`[${serverName}] Using proxychains4 proxy: ${proxyConfig.type || 'socks5'}://${proxyConfig.host}:${proxyConfig.port}`);
193
207
  return {
194
208
  command: proxychains4Path,
195
209
  args: ['-f', configPath, command, ...sanitizedArgs],
@@ -237,7 +251,7 @@ export const getMcpServer = async (sessionId, group) => {
237
251
  servers[sessionId] = createMcpServer(descriptor.name, descriptor.version, descriptor);
238
252
  }
239
253
  else {
240
- console.log(`MCP server already exists for sessionId: ${sessionId}`);
254
+ logger.log(`MCP server already exists for sessionId: ${sessionId}`);
241
255
  }
242
256
  return servers[sessionId];
243
257
  };
@@ -255,7 +269,7 @@ const closeIsolatedClient = (serverName, client, transport) => {
255
269
  client.close();
256
270
  }
257
271
  catch (e) {
258
- console.warn(`[${serverName}] Error closing isolated client:`, e);
272
+ logger.warn(`[${serverName}] Error closing isolated client:`, e);
259
273
  }
260
274
  const candidateTransport = transport;
261
275
  const stdioPid = typeof candidateTransport.pid === 'number' ? candidateTransport.pid : null;
@@ -263,7 +277,7 @@ const closeIsolatedClient = (serverName, client, transport) => {
263
277
  transport.close();
264
278
  }
265
279
  catch (e) {
266
- console.warn(`[${serverName}] Error closing isolated transport:`, e);
280
+ logger.warn(`[${serverName}] Error closing isolated transport:`, e);
267
281
  }
268
282
  // For stdio transports, kill the whole process tree to avoid orphans
269
283
  if (stdioPid) {
@@ -287,7 +301,7 @@ const cleanupIsolatedSession = (sessionId) => {
287
301
  isolatedClientCreationLocks.delete(key);
288
302
  }
289
303
  }
290
- console.log(`Cleaned up isolated clients for session: ${sessionId}`);
304
+ logger.log(`Cleaned up isolated clients for session: ${sessionId}`);
291
305
  }
292
306
  // Drop per-session OpenAPI cookie jars so authenticated state does not
293
307
  // outlive the downstream session.
@@ -355,12 +369,12 @@ const getOrCreateIsolatedClient = async (sessionId, serverInfo) => {
355
369
  // Guard: the session was cleaned up during the async connect if its map was
356
370
  // deleted (identity no longer matches) — close the just-created connection.
357
371
  if (sessionIsolatedClients.get(sessionId) !== reservedClients) {
358
- console.warn(`Session ${sessionId} was deleted during isolated client creation for ${serverInfo.name}, closing new client`);
372
+ logger.warn(`Session ${sessionId} was deleted during isolated client creation for ${serverInfo.name}, closing new client`);
359
373
  closeIsolatedClient(serverInfo.name, client, transport);
360
374
  throw new Error(`Session ${sessionId} no longer exists`);
361
375
  }
362
376
  setSessionIsolatedClient(sessionId, serverInfo.name, client, transport);
363
- console.log(`Created isolated client for session ${sessionId} -> ${serverInfo.name}`);
377
+ logger.log(`Created isolated client for session ${sessionId} -> ${serverInfo.name}`);
364
378
  return { client, transport };
365
379
  })();
366
380
  isolatedClientCreationLocks.set(lockKey, createPromise);
@@ -382,10 +396,10 @@ const broadcastListChanged = (listType, sendNotification) => {
382
396
  Object.values(servers).forEach((server) => {
383
397
  sendNotification(server)
384
398
  .then(() => {
385
- console.log(`${listType} list changed notification sent successfully`);
399
+ logger.log(`${listType} list changed notification sent successfully`);
386
400
  })
387
401
  .catch((error) => {
388
- console.warn(`Failed to send ${listType} list changed notification:`, error.message);
402
+ logger.warn(`Failed to send ${listType} list changed notification:`, error.message);
389
403
  });
390
404
  });
391
405
  };
@@ -416,12 +430,12 @@ export const updateServerInfoVisibility = (serverName, visibility, sharedWithUse
416
430
  export const syncToolEmbedding = async (serverName, toolName) => {
417
431
  const serverInfo = getServerByName(serverName);
418
432
  if (!serverInfo) {
419
- console.warn(`Server not found: ${serverName}`);
433
+ logger.warn(`Server not found: ${serverName}`);
420
434
  return;
421
435
  }
422
436
  const tool = serverInfo.tools.find((t) => t.name === toolName);
423
437
  if (!tool) {
424
- console.warn(`Tool not found: ${toolName} on server: ${serverName}`);
438
+ logger.warn(`Tool not found: ${toolName} on server: ${serverName}`);
425
439
  return;
426
440
  }
427
441
  if (isAppOnlyTool(tool)) {
@@ -429,8 +443,8 @@ export const syncToolEmbedding = async (serverName, toolName) => {
429
443
  }
430
444
  // Save tool as vector embedding for search
431
445
  syncToolsAsVectorEmbeddings(serverName, [tool]).catch((error) => {
432
- console.warn(`[EMBED_SYNC_ERROR] Failed to sync embedding for tool "${toolName}" on server "${serverName}"`);
433
- console.error('Error syncing single tool embedding', { serverName, toolName, error });
446
+ logger.warn(`[EMBED_SYNC_ERROR] Failed to sync embedding for tool "${toolName}" on server "${serverName}"`);
447
+ logger.error('Error syncing single tool embedding', { serverName, toolName, error });
434
448
  });
435
449
  };
436
450
  // Helper function to clean $schema field from inputSchema
@@ -540,7 +554,7 @@ export const updateServerToolsCache = (serverInfo, tools, options) => {
540
554
  syncToolsAsVectorEmbeddings(serverInfo.name, serverInfo.tools, {
541
555
  reportProgress: options?.reportEmbeddingProgress === true,
542
556
  }).catch(() => {
543
- console.warn('[EMBED_SYNC_ERROR] Failed to sync tool embeddings');
557
+ logger.warn('[EMBED_SYNC_ERROR] Failed to sync tool embeddings');
544
558
  });
545
559
  };
546
560
  const updateServerPromptsCache = (serverInfo, prompts) => {
@@ -550,7 +564,7 @@ const updateServerResourcesCache = (serverInfo, resources) => {
550
564
  serverInfo.resources = resources.map(normalizeResourceForCache);
551
565
  };
552
566
  const logListChangedRefreshError = (listType) => {
553
- console.warn(`Failed to refresh ${listType} list after upstream notification`);
567
+ logger.warn(`Failed to refresh ${listType} list after upstream notification`);
554
568
  };
555
569
  const createUpstreamMcpClient = (name, getServerInfo) => {
556
570
  return new Client({
@@ -661,7 +675,7 @@ export const cleanupAllServers = () => {
661
675
  }
662
676
  }
663
677
  catch (error) {
664
- console.warn('Error closing server', { serverName: serverInfo.name, error });
678
+ logger.warn('Error closing server', { serverName: serverInfo.name, error });
665
679
  }
666
680
  }
667
681
  serverInfos = [];
@@ -920,9 +934,7 @@ export const createTransportFromConfig = async (name, conf) => {
920
934
  // Admin-owned servers may legitimately target internal services, so they
921
935
  // skip the internal-IP blocklist. allowInternal also governs per-hop
922
936
  // redirect validation in createRedirectValidatingFetch below.
923
- const ownerUser = conf.owner
924
- ? await getUserDao().findByUsername(conf.owner)
925
- : null;
937
+ const ownerUser = conf.owner ? await getUserDao().findByUsername(conf.owner) : null;
926
938
  const allowInternal = !!ownerUser?.isAdmin;
927
939
  if (conf.url) {
928
940
  await assertSafeUrl(conf.url, { allowInternal });
@@ -943,7 +955,7 @@ export const createTransportFromConfig = async (name, conf) => {
943
955
  // Authorization value would override the valid access token and cause
944
956
  // 401 re-authorization loops.
945
957
  headers = stripAuthorizationHeader(headers);
946
- console.log(`OAuth provider configured for server: ${name}`);
958
+ logger.log(`OAuth provider configured for server: ${name}`);
947
959
  }
948
960
  if (Object.keys(headers).length > 0) {
949
961
  options.requestInit = {
@@ -965,7 +977,7 @@ export const createTransportFromConfig = async (name, conf) => {
965
977
  options.authProvider = authProvider;
966
978
  // Drop static Authorization header when OAuth manages auth (see above).
967
979
  headers = stripAuthorizationHeader(headers);
968
- console.log(`OAuth provider configured for server: ${name}`);
980
+ logger.log(`OAuth provider configured for server: ${name}`);
969
981
  }
970
982
  if (Object.keys(headers).length > 0) {
971
983
  options.eventSourceInit = {
@@ -1009,7 +1021,7 @@ export const createTransportFromConfig = async (name, conf) => {
1009
1021
  if (pendingReinstalls.has(name)) {
1010
1022
  resolvedArgs = injectRefreshFlag(conf.command, resolvedArgs);
1011
1023
  pendingReinstalls.delete(name);
1012
- console.log(`[${name}] Injected cache refresh flags for reinstall`);
1024
+ logger.log(`[${name}] Injected cache refresh flags for reinstall`);
1013
1025
  }
1014
1026
  const { command: finalCommand, args: finalArgs } = wrapWithProxychains(name, conf.command, resolvedArgs, conf.proxy);
1015
1027
  // Create STDIO transport with potentially wrapped command
@@ -1022,10 +1034,10 @@ export const createTransportFromConfig = async (name, conf) => {
1022
1034
  });
1023
1035
  if (transport.stderr) {
1024
1036
  observeStdioStderr(transport, transport.stderr, (message) => {
1025
- console.log('Upstream server stderr', {
1037
+ logger.log('Upstream server stderr', JSON.stringify({
1026
1038
  serverName: name,
1027
1039
  message,
1028
- });
1040
+ }));
1029
1041
  });
1030
1042
  }
1031
1043
  }
@@ -1057,7 +1069,7 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
1057
1069
  const isStreamableHttp = transport instanceof StreamableHTTPClientTransport;
1058
1070
  const isSSE = transport instanceof SSEClientTransport;
1059
1071
  if (attempt < maxRetries && transport && ((isStreamableHttp && isHttp40xError) || isSSE)) {
1060
- console.warn(`${isHttp40xError ? 'HTTP 40x error' : 'error'} detected for ${isStreamableHttp ? 'StreamableHTTP' : 'SSE'} server ${serverInfo.name}${isolated ? ` (isolated session ${isolated.sessionId})` : ''}, attempting reconnection (attempt ${attempt + 1}/${maxRetries + 1})`);
1072
+ logger.warn(`${isHttp40xError ? 'HTTP 40x error' : 'error'} detected for ${isStreamableHttp ? 'StreamableHTTP' : 'SSE'} server ${serverInfo.name}${isolated ? ` (isolated session ${isolated.sessionId})` : ''}, attempting reconnection (attempt ${attempt + 1}/${maxRetries + 1})`);
1061
1073
  try {
1062
1074
  const server = await getServerDao().findById(serverInfo.name);
1063
1075
  if (!server) {
@@ -1074,11 +1086,15 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
1074
1086
  try {
1075
1087
  client.close();
1076
1088
  }
1077
- catch { /* empty */ }
1089
+ catch {
1090
+ /* empty */
1091
+ }
1078
1092
  try {
1079
1093
  transport.close();
1080
1094
  }
1081
- catch { /* empty */ }
1095
+ catch {
1096
+ /* empty */
1097
+ }
1082
1098
  setSessionIsolatedClient(isolated.sessionId, serverInfo.name, newClient, newTransport);
1083
1099
  }
1084
1100
  else {
@@ -1090,11 +1106,15 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
1090
1106
  try {
1091
1107
  serverInfo.client?.close();
1092
1108
  }
1093
- catch { /* empty */ }
1109
+ catch {
1110
+ /* empty */
1111
+ }
1094
1112
  try {
1095
1113
  transport.close();
1096
1114
  }
1097
- catch { /* empty */ }
1115
+ catch {
1116
+ /* empty */
1117
+ }
1098
1118
  serverInfo.client = newClient;
1099
1119
  serverInfo.transport = newTransport;
1100
1120
  serverInfo.status = 'connected';
@@ -1108,18 +1128,18 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
1108
1128
  updateServerToolsCache(serverInfo, tools.tools);
1109
1129
  }
1110
1130
  catch (listToolsError) {
1111
- console.warn('Failed to reload tools after reconnection', {
1131
+ logger.warn('Failed to reload tools after reconnection', {
1112
1132
  serverName: serverInfo.name,
1113
1133
  error: summarizeErrorForLogging(listToolsError),
1114
1134
  });
1115
1135
  // Continue anyway, as the connection might still work for the current tool
1116
1136
  }
1117
- console.log(`Successfully reconnected to server: ${serverInfo.name}`);
1137
+ logger.log(`Successfully reconnected to server: ${serverInfo.name}`);
1118
1138
  // Continue to next attempt
1119
1139
  continue;
1120
1140
  }
1121
1141
  catch (reconnectError) {
1122
- console.error('Failed to reconnect to server', {
1142
+ logger.error('Failed to reconnect to server', {
1123
1143
  serverName: serverInfo.name,
1124
1144
  error: summarizeErrorForLogging(reconnectError),
1125
1145
  });
@@ -1153,7 +1173,7 @@ const setupServerKeepAlive = (serverInfo, serverConfig) => {
1153
1173
  throw error;
1154
1174
  }
1155
1175
  },
1156
- }).catch((error) => console.warn('Keepalive setup failed', {
1176
+ }).catch((error) => logger.warn('Keepalive setup failed', {
1157
1177
  serverName: serverInfo.name,
1158
1178
  error: summarizeErrorForLogging(error),
1159
1179
  }));
@@ -1166,11 +1186,20 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
1166
1186
  try {
1167
1187
  for (const conf of allServers) {
1168
1188
  const { name } = conf;
1189
+ // Names loaded from disk are not rejected (that would break existing
1190
+ // configs on upgrade), but a strict client may drop the whole tools/list
1191
+ // if the server name produces a non-conforming downstream tool name.
1192
+ if (isInit) {
1193
+ const nameValidation = validateServerName(name);
1194
+ if (!nameValidation.valid) {
1195
+ logger.warn(`Server name '${name}' does not match the MCP tool-name charset (${nameValidation.message}); downstream tool names may be rejected by strict clients`);
1196
+ }
1197
+ }
1169
1198
  // Expand environment variables in all configuration values
1170
1199
  const expandedConf = replaceEnvVars(conf);
1171
1200
  // Skip disabled servers
1172
1201
  if (expandedConf.enabled === false) {
1173
- console.log(`Skipping disabled server: ${name}`);
1202
+ logger.log(`Skipping disabled server: ${name}`);
1174
1203
  nextServerInfos.push({
1175
1204
  name,
1176
1205
  owner: expandedConf.owner,
@@ -1210,7 +1239,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
1210
1239
  enabled: true,
1211
1240
  config: expandedConf,
1212
1241
  });
1213
- console.log(`Skipping startup connect for on-demand server: ${name}`);
1242
+ logger.log(`Skipping startup connect for on-demand server: ${name}`);
1214
1243
  continue;
1215
1244
  }
1216
1245
  // Reuse this server's existing runtime state instead of reconnecting when:
@@ -1226,8 +1255,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
1226
1255
  const existingServer = existingServerInfos.find((s) => s.name === name);
1227
1256
  const isDifferentServer = Boolean(serverName) && serverName !== name;
1228
1257
  const hasInflightOAuthAuthorization = existingServer?.status === 'oauth_required' &&
1229
- Boolean(expandedConf.oauth?.pendingAuthorization?.state ||
1230
- existingServer.oauth?.state);
1258
+ Boolean(expandedConf.oauth?.pendingAuthorization?.state || existingServer.oauth?.state);
1231
1259
  if (existingServer &&
1232
1260
  (isDifferentServer ||
1233
1261
  (!serverName && (existingServer.status === 'connected' || hasInflightOAuthAuthorization)))) {
@@ -1238,7 +1266,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
1238
1266
  sharedWithUsers: expandedConf.sharedWithUsers,
1239
1267
  enabled: expandedConf.enabled === undefined ? true : expandedConf.enabled,
1240
1268
  });
1241
- console.log(hasInflightOAuthAuthorization
1269
+ logger.log(hasInflightOAuthAuthorization
1242
1270
  ? `Server '${name}' has an in-flight OAuth authorization; preserving existing state.`
1243
1271
  : `Server '${name}' is already connected.`);
1244
1272
  continue;
@@ -1248,7 +1276,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
1248
1276
  if (expandedConf.type === 'openapi') {
1249
1277
  // Handle OpenAPI type servers
1250
1278
  if (!expandedConf.openapi?.url && !expandedConf.openapi?.schema) {
1251
- console.warn(`Skipping OpenAPI server '${name}': missing OpenAPI specification URL or schema`);
1279
+ logger.warn(`Skipping OpenAPI server '${name}': missing OpenAPI specification URL or schema`);
1252
1280
  nextServerInfos.push({
1253
1281
  name,
1254
1282
  owner: expandedConf.owner,
@@ -1301,7 +1329,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
1301
1329
  });
1302
1330
  },
1303
1331
  });
1304
- console.log(`Initializing OpenAPI server: ${name}...`);
1332
+ logger.log(`Initializing OpenAPI server: ${name}...`);
1305
1333
  // Perform async initialization
1306
1334
  await openApiClient.initialize();
1307
1335
  // Convert OpenAPI tools to MCP tool format
@@ -1315,7 +1343,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
1315
1343
  serverInfo.status = 'connected';
1316
1344
  serverInfo.tools = mcpTools;
1317
1345
  serverInfo.openApiClient = openApiClient;
1318
- console.log(`Successfully initialized OpenAPI server: ${name} with ${mcpTools.length} tools`);
1346
+ logger.log(`Successfully initialized OpenAPI server: ${name} with ${mcpTools.length} tools`);
1319
1347
  // Broadcast now that tools are loaded. OpenAPI servers expose tools
1320
1348
  // only (no prompts/resources). See the standard-path note above.
1321
1349
  broadcastToolListChanged();
@@ -1323,8 +1351,8 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
1323
1351
  syncToolsAsVectorEmbeddings(name, mcpTools, {
1324
1352
  reportProgress: options?.reportEmbeddingProgress === true && serverName === name,
1325
1353
  }).catch((error) => {
1326
- console.warn(`[EMBED_SYNC_ERROR] Failed to sync OpenAPI embeddings for server "${name}"`);
1327
- console.error('Error syncing OpenAPI tool embeddings', {
1354
+ logger.warn(`[EMBED_SYNC_ERROR] Failed to sync OpenAPI embeddings for server "${name}"`);
1355
+ logger.error('Error syncing OpenAPI tool embeddings', {
1328
1356
  serverName: name,
1329
1357
  error: summarizeErrorForLogging(error),
1330
1358
  });
@@ -1332,7 +1360,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
1332
1360
  continue;
1333
1361
  }
1334
1362
  catch (error) {
1335
- console.error('Failed to initialize OpenAPI server', {
1363
+ logger.error('Failed to initialize OpenAPI server', {
1336
1364
  serverName: name,
1337
1365
  error: summarizeErrorForLogging(error),
1338
1366
  });
@@ -1391,18 +1419,18 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
1391
1419
  nextServerInfos.push(serverInfo);
1392
1420
  connectClientWithDiagnostics(client, transport, initRequestOptions || requestOptions)
1393
1421
  .then(() => {
1394
- console.log(`Successfully connected client for server: ${name}`);
1422
+ logger.log(`Successfully connected client for server: ${name}`);
1395
1423
  const serverVersion = client.getServerVersion?.();
1396
1424
  serverInfo.version = serverVersion?.version;
1397
1425
  serverInfo.instructions = client.getInstructions?.();
1398
1426
  const capabilities = client.getServerCapabilities();
1399
- console.log('Server capabilities', JSON.stringify(capabilities));
1427
+ logger.log('Server capabilities', JSON.stringify(capabilities));
1400
1428
  let dataError = null;
1401
1429
  if (capabilities?.tools) {
1402
1430
  client
1403
1431
  .listTools({}, initRequestOptions || requestOptions)
1404
1432
  .then((tools) => {
1405
- console.log(`Successfully listed ${tools.tools.length} tools for server: ${name}`);
1433
+ logger.log(`Successfully listed ${tools.tools.length} tools for server: ${name}`);
1406
1434
  updateServerToolsCache(serverInfo, tools.tools, {
1407
1435
  reportEmbeddingProgress: options?.reportEmbeddingProgress === true && serverName === name,
1408
1436
  });
@@ -1413,7 +1441,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
1413
1441
  broadcastToolListChanged();
1414
1442
  })
1415
1443
  .catch((error) => {
1416
- console.error('Failed to list tools for server', {
1444
+ logger.error('Failed to list tools for server', {
1417
1445
  serverName: name,
1418
1446
  error: summarizeErrorForLogging(error),
1419
1447
  });
@@ -1424,12 +1452,12 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
1424
1452
  client
1425
1453
  .listPrompts({}, initRequestOptions || requestOptions)
1426
1454
  .then((prompts) => {
1427
- console.log(`Successfully listed ${prompts.prompts.length} prompts for server: ${name}`);
1455
+ logger.log(`Successfully listed ${prompts.prompts.length} prompts for server: ${name}`);
1428
1456
  updateServerPromptsCache(serverInfo, prompts.prompts);
1429
1457
  broadcastPromptListChanged();
1430
1458
  })
1431
1459
  .catch((error) => {
1432
- console.error('Failed to list prompts for server', {
1460
+ logger.error('Failed to list prompts for server', {
1433
1461
  serverName: name,
1434
1462
  error: summarizeErrorForLogging(error),
1435
1463
  });
@@ -1440,12 +1468,12 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
1440
1468
  client
1441
1469
  .listResources({}, initRequestOptions || requestOptions)
1442
1470
  .then((resources) => {
1443
- console.log(`Successfully listed ${resources.resources.length} resources for server: ${name}`);
1471
+ logger.log(`Successfully listed ${resources.resources.length} resources for server: ${name}`);
1444
1472
  updateServerResourcesCache(serverInfo, resources.resources);
1445
1473
  broadcastResourceListChanged();
1446
1474
  })
1447
1475
  .catch((error) => {
1448
- console.error('Failed to list resources for server', {
1476
+ logger.error('Failed to list resources for server', {
1449
1477
  serverName: name,
1450
1478
  error: summarizeErrorForLogging(error),
1451
1479
  });
@@ -1471,7 +1499,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
1471
1499
  if (isOAuthError) {
1472
1500
  // OAuth provider should have already set the status to 'oauth_required'
1473
1501
  // and stored the authorization URL in serverInfo.oauth
1474
- console.log(`OAuth authorization required for server ${name}. Status should be set to 'oauth_required'.`);
1502
+ logger.log(`OAuth authorization required for server ${name}. Status should be set to 'oauth_required'.`);
1475
1503
  // Make sure status is set correctly
1476
1504
  if (serverInfo.status !== 'oauth_required') {
1477
1505
  serverInfo.status = 'oauth_required';
@@ -1479,7 +1507,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
1479
1507
  serverInfo.error = null;
1480
1508
  }
1481
1509
  else {
1482
- console.error('Failed to connect client for server', {
1510
+ logger.error('Failed to connect client for server', {
1483
1511
  serverName: name,
1484
1512
  error: summarizeErrorForLogging(error),
1485
1513
  });
@@ -1489,7 +1517,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
1489
1517
  setupServerKeepAlive(serverInfo, expandedConf);
1490
1518
  }
1491
1519
  });
1492
- console.log(`Initialized client for server: ${name}`);
1520
+ logger.log(`Initialized client for server: ${name}`);
1493
1521
  }
1494
1522
  }
1495
1523
  catch (error) {
@@ -1650,14 +1678,14 @@ export const getServerByOAuthState = (state) => {
1650
1678
  * This will close the existing connection and reinitialize the server
1651
1679
  */
1652
1680
  export const reconnectServer = async (serverName) => {
1653
- console.log(`Reconnecting server: ${serverName}`);
1681
+ logger.log(`Reconnecting server: ${serverName}`);
1654
1682
  const serverInfo = getServerByName(serverName);
1655
1683
  if (!serverInfo) {
1656
1684
  throw new Error(`Server not found: ${serverName}`);
1657
1685
  }
1658
1686
  const serverConfig = await getServerDao().findById(serverName);
1659
1687
  if (serverConfig?.enabled === false) {
1660
- console.log(`Skipping reconnect for disabled server: ${serverName}`);
1688
+ logger.log(`Skipping reconnect for disabled server: ${serverName}`);
1661
1689
  return;
1662
1690
  }
1663
1691
  // Close existing connection if any
@@ -1666,7 +1694,7 @@ export const reconnectServer = async (serverName) => {
1666
1694
  serverInfo.client.close();
1667
1695
  }
1668
1696
  catch (error) {
1669
- console.warn('Error closing client for server', { serverName, error });
1697
+ logger.warn('Error closing client for server', { serverName, error });
1670
1698
  }
1671
1699
  }
1672
1700
  if (serverInfo.transport) {
@@ -1674,7 +1702,7 @@ export const reconnectServer = async (serverName) => {
1674
1702
  serverInfo.transport.close();
1675
1703
  }
1676
1704
  catch (error) {
1677
- console.warn('Error closing transport for server', { serverName, error });
1705
+ logger.warn('Error closing transport for server', { serverName, error });
1678
1706
  }
1679
1707
  }
1680
1708
  if (serverInfo.keepAliveIntervalId) {
@@ -1683,13 +1711,13 @@ export const reconnectServer = async (serverName) => {
1683
1711
  }
1684
1712
  // Reinitialize the server
1685
1713
  await initializeClientsFromSettings(false, serverName);
1686
- console.log(`Successfully reconnected server: ${serverName}`);
1714
+ logger.log(`Successfully reconnected server: ${serverName}`);
1687
1715
  };
1688
1716
  // Reinstall server: clear package cache and reconnect.
1689
1717
  // For npx: deletes ~/.npm/_npx before reconnect (--ignore-existing removed in npm 7+).
1690
1718
  // For uvx: schedules --refresh flag injection on next spawn via pendingReinstalls Set.
1691
1719
  export const reinstallServer = async (serverName) => {
1692
- console.log(`Reinstalling server: ${serverName}`);
1720
+ logger.log(`Reinstalling server: ${serverName}`);
1693
1721
  const serverInfo = getServerByName(serverName);
1694
1722
  if (!serverInfo) {
1695
1723
  throw new Error(`Server not found: ${serverName}`);
@@ -1713,7 +1741,7 @@ export const reinstallServer = async (serverName) => {
1713
1741
  await clearRunnerCache(command);
1714
1742
  // Close and reconnect (will pick up pendingReinstalls flag for uvx)
1715
1743
  await reconnectServer(serverName);
1716
- console.log(`Successfully initiated reinstall for server: ${serverName}`);
1744
+ logger.log(`Successfully initiated reinstall for server: ${serverName}`);
1717
1745
  }
1718
1746
  catch (error) {
1719
1747
  // Clean up pendingReinstalls on failure to avoid stale entries
@@ -1738,6 +1766,42 @@ const filterToolsByConfig = async (serverName, tools) => {
1738
1766
  const getServerByTool = (toolName) => {
1739
1767
  return getVisibleServerInfos().find((serverInfo) => serverInfo.enabled !== false && serverInfo.tools.some((tool) => tool.name === toolName));
1740
1768
  };
1769
+ /**
1770
+ * Error thrown when the requested server/tool (or server/prompt) cannot be
1771
+ * used by the current caller. `message` is the single safe text returned to
1772
+ * the caller — intentionally identical whether the target is hidden from them
1773
+ * or does not exist at all, so server/tool existence is never leaked through
1774
+ * distinguishable authorization errors (see #1103). `reason` is an internal
1775
+ * diagnostic recorded only in logs/activity, never surfaced to the caller.
1776
+ */
1777
+ export class ToolUnavailableError extends Error {
1778
+ constructor(message, reason) {
1779
+ super(message);
1780
+ this.reason = reason;
1781
+ this.name = 'ToolUnavailableError';
1782
+ }
1783
+ }
1784
+ /**
1785
+ * Best-effort internal classification of why a qualified tool/prompt name was
1786
+ * unavailable, for logs/activity only. The externally visible message must not
1787
+ * vary with this result.
1788
+ */
1789
+ const classifyUnavailableReason = (qualifiedName) => {
1790
+ // Qualified names are `<server><separator><name>`; a bare name has no prefix.
1791
+ const serverName = qualifiedName.split(getNameSeparator())[0];
1792
+ const server = getServerByName(serverName);
1793
+ if (!server) {
1794
+ return 'server-or-name-not-found';
1795
+ }
1796
+ if (server.enabled === false) {
1797
+ return 'server-disabled';
1798
+ }
1799
+ // Server exists and is enabled: distinguish a caller-hidden server from a
1800
+ // tool/prompt that is missing (or group-filtered) on a visible server.
1801
+ return getVisibleServerByName(serverName)
1802
+ ? 'tool-or-prompt-not-found'
1803
+ : 'server-hidden-from-caller';
1804
+ };
1741
1805
  // Add new server
1742
1806
  export const addServer = async (name, config) => {
1743
1807
  const server = { name, ...config };
@@ -1764,7 +1828,7 @@ export const removeServer = async (name) => {
1764
1828
  await removeServerToolEmbeddings(name);
1765
1829
  }
1766
1830
  catch (error) {
1767
- console.warn('Failed to remove embeddings for server', { serverName: name, error });
1831
+ logger.warn('Failed to remove embeddings for server', { serverName: name, error });
1768
1832
  }
1769
1833
  serverInfos = serverInfos.filter((serverInfo) => serverInfo.name !== name);
1770
1834
  return { success: true, message: 'Server removed successfully' };
@@ -1793,7 +1857,7 @@ export const addOrUpdateServer = async (name, config, allowOverride = false) =>
1793
1857
  return { success: true, message: `Server ${action} successfully` };
1794
1858
  }
1795
1859
  catch (error) {
1796
- console.error('Failed to add/update server', { serverName: name, error });
1860
+ logger.error('Failed to add/update server', { serverName: name, error });
1797
1861
  return { success: false, message: 'Failed to add/update server' };
1798
1862
  }
1799
1863
  };
@@ -1823,7 +1887,7 @@ const closeServerRuntime = (serverInfo) => {
1823
1887
  if (serverInfo.keepAliveIntervalId) {
1824
1888
  clearInterval(serverInfo.keepAliveIntervalId);
1825
1889
  serverInfo.keepAliveIntervalId = undefined;
1826
- console.log('Cleared MCP server keep-alive interval');
1890
+ logger.log('Cleared MCP server keep-alive interval');
1827
1891
  }
1828
1892
  const candidateTransport = serverInfo.transport;
1829
1893
  const stdioPid = typeof candidateTransport?.pid === 'number' ? candidateTransport.pid : null;
@@ -1832,7 +1896,7 @@ const closeServerRuntime = (serverInfo) => {
1832
1896
  serverInfo.client.close();
1833
1897
  }
1834
1898
  catch {
1835
- console.warn('Error closing MCP client during runtime shutdown');
1899
+ logger.warn('Error closing MCP client during runtime shutdown');
1836
1900
  }
1837
1901
  serverInfo.client = undefined;
1838
1902
  }
@@ -1841,14 +1905,14 @@ const closeServerRuntime = (serverInfo) => {
1841
1905
  serverInfo.transport.close();
1842
1906
  }
1843
1907
  catch {
1844
- console.warn('Error closing MCP transport during runtime shutdown');
1908
+ logger.warn('Error closing MCP transport during runtime shutdown');
1845
1909
  }
1846
1910
  serverInfo.transport = undefined;
1847
1911
  }
1848
1912
  if (stdioPid) {
1849
1913
  killStdioProcessTree(serverInfo.name, stdioPid);
1850
1914
  }
1851
- console.log('Closed MCP server client and transport');
1915
+ logger.log('Closed MCP server client and transport');
1852
1916
  };
1853
1917
  // Close server client and transport (keeps the entry in serverInfos; the next
1854
1918
  // initializeClientsFromSettings rebuild drops it if it is no longer configured).
@@ -1874,7 +1938,7 @@ const shutdownOnDemandServer = (serverInfo) => {
1874
1938
  // Close runtime (kills process) but intentionally keep tools/prompts/resources
1875
1939
  closeServerRuntime(serverInfo);
1876
1940
  serverInfo.status = 'disconnected';
1877
- console.log(`[${serverInfo.name}] On-demand server shut down after idle; ` +
1941
+ logger.log(`[${serverInfo.name}] On-demand server shut down after idle; ` +
1878
1942
  `${serverInfo.tools.length} tools cached for next wake-up`);
1879
1943
  };
1880
1944
  /**
@@ -1932,7 +1996,7 @@ const ensureServerReady = async (serverInfo) => {
1932
1996
  return;
1933
1997
  }
1934
1998
  const name = rawConfig.name;
1935
- console.log(`[${name}] Cold-starting on-demand server…`);
1999
+ logger.log(`[${name}] Cold-starting on-demand server…`);
1936
2000
  const transport = await createTransportFromConfig(name, expandedConf);
1937
2001
  const client = createUpstreamMcpClient(name, () => serverInfo);
1938
2002
  const serverRequestOptions = expandedConf.options || {};
@@ -1965,7 +2029,7 @@ const ensureServerReady = async (serverInfo) => {
1965
2029
  broadcastToolListChanged();
1966
2030
  }
1967
2031
  catch (error) {
1968
- console.warn(`[${name}] Failed to list tools during wake`, {
2032
+ logger.warn(`[${name}] Failed to list tools during wake`, {
1969
2033
  error: summarizeErrorForLogging(error),
1970
2034
  });
1971
2035
  }
@@ -1977,7 +2041,7 @@ const ensureServerReady = async (serverInfo) => {
1977
2041
  updateServerPromptsCache(serverInfo, prompts.prompts);
1978
2042
  }
1979
2043
  catch (error) {
1980
- console.warn(`[${name}] Failed to list prompts during wake`, {
2044
+ logger.warn(`[${name}] Failed to list prompts during wake`, {
1981
2045
  error: summarizeErrorForLogging(error),
1982
2046
  });
1983
2047
  }
@@ -1989,14 +2053,14 @@ const ensureServerReady = async (serverInfo) => {
1989
2053
  updateServerResourcesCache(serverInfo, resources.resources);
1990
2054
  }
1991
2055
  catch (error) {
1992
- console.warn(`[${name}] Failed to list resources during wake`, {
2056
+ logger.warn(`[${name}] Failed to list resources during wake`, {
1993
2057
  error: summarizeErrorForLogging(error),
1994
2058
  });
1995
2059
  }
1996
2060
  }
1997
2061
  // No-op for stdio transports; only SSE/streamable-http opt in via enableKeepAlive.
1998
2062
  setupServerKeepAlive(serverInfo, expandedConf);
1999
- console.log(`[${name}] On-demand server ready in ${Date.now() - spawnStart}ms` +
2063
+ logger.log(`[${name}] On-demand server ready in ${Date.now() - spawnStart}ms` +
2000
2064
  ` (${toolCount} tools, ${promptCount} prompts, ${resourceCount} resources)`);
2001
2065
  })();
2002
2066
  serverInfo.spawningPromise = spawnPromise;
@@ -2035,16 +2099,16 @@ const primeOnDemandServers = () => {
2035
2099
  si.enabled !== false);
2036
2100
  if (targets.length === 0)
2037
2101
  return Promise.resolve();
2038
- console.log(`Priming ${targets.length} on-demand server(s) for tool discovery…`);
2102
+ logger.log(`Priming ${targets.length} on-demand server(s) for tool discovery…`);
2039
2103
  return Promise.allSettled(targets.map(async (si) => {
2040
2104
  try {
2041
2105
  await ensureServerReady(si);
2042
2106
  // Sleep the child but keep the cached tool list visible to agents.
2043
2107
  shutdownOnDemandServer(si);
2044
- console.log('On-demand server primed and asleep');
2108
+ logger.log('On-demand server primed and asleep');
2045
2109
  }
2046
2110
  catch (error) {
2047
- console.warn('Failed to prime on-demand server', {
2111
+ logger.warn('Failed to prime on-demand server', {
2048
2112
  error: summarizeErrorForLogging(error),
2049
2113
  });
2050
2114
  }
@@ -2073,7 +2137,28 @@ export const resetServerOAuthConnection = (name) => {
2073
2137
  // launched through a wrapper like `npx` / `npm exec`, the wrapper does not
2074
2138
  // forward signals to its descendants, so the real server process is left
2075
2139
  // running as an orphan. Walk the whole tree and force-kill it.
2140
+ //
2141
+ // When the process-lister tool tree-kill needs (`ps` on Linux) is missing —
2142
+ // e.g. slim Docker images without procps — tree-kill's internal spawn fails
2143
+ // with an unhandled 'error' event that would crash MCPHub. Fall back to
2144
+ // signaling just the direct child instead (issue #1072).
2076
2145
  function killStdioProcessTree(name, pid) {
2146
+ const safeDirectKill = (signal) => {
2147
+ try {
2148
+ process.kill(pid, signal);
2149
+ }
2150
+ catch (err) {
2151
+ const code = err.code;
2152
+ if (code !== 'ESRCH') {
2153
+ logger.warn('Failed to send signal to process', {
2154
+ serverName: name,
2155
+ pid,
2156
+ signal,
2157
+ err,
2158
+ });
2159
+ }
2160
+ }
2161
+ };
2077
2162
  const safeTreeKill = (signal) => {
2078
2163
  try {
2079
2164
  treeKill(pid, signal, (err) => {
@@ -2085,7 +2170,7 @@ function killStdioProcessTree(name, pid) {
2085
2170
  // Pass the user-controlled `name` as a separate argument so a
2086
2171
  // server named e.g. "%s" cannot inject format specifiers into the
2087
2172
  // log line (CodeQL: use-of-externally-controlled-format-string).
2088
- console.warn('Failed to send signal to process tree', {
2173
+ logger.warn('Failed to send signal to process tree', {
2089
2174
  serverName: name,
2090
2175
  pid,
2091
2176
  signal,
@@ -2096,7 +2181,7 @@ function killStdioProcessTree(name, pid) {
2096
2181
  });
2097
2182
  }
2098
2183
  catch (err) {
2099
- console.warn('Failed to send signal to process tree', {
2184
+ logger.warn('Failed to send signal to process tree', {
2100
2185
  serverName: name,
2101
2186
  pid,
2102
2187
  signal,
@@ -2104,12 +2189,22 @@ function killStdioProcessTree(name, pid) {
2104
2189
  });
2105
2190
  }
2106
2191
  };
2107
- safeTreeKill('SIGTERM');
2192
+ const safeKill = isProcessTreeKillAvailable()
2193
+ ? safeTreeKill
2194
+ : (signal) => {
2195
+ logger.warn('Process lister unavailable, killing only the direct child process', {
2196
+ serverName: name,
2197
+ pid,
2198
+ signal,
2199
+ });
2200
+ safeDirectKill(signal);
2201
+ };
2202
+ safeKill('SIGTERM');
2108
2203
  setTimeout(() => {
2109
2204
  if (!isProcessAlive(pid)) {
2110
2205
  return;
2111
2206
  }
2112
- safeTreeKill('SIGKILL');
2207
+ safeKill('SIGKILL');
2113
2208
  }, STDIO_KILL_GRACE_PERIOD_MS);
2114
2209
  }
2115
2210
  function isProcessAlive(pid) {
@@ -2143,10 +2238,10 @@ export const toggleServerStatus = async (name, enabled) => {
2143
2238
  // Remove tool embeddings when server is disabled (for smart routing consistency)
2144
2239
  try {
2145
2240
  await removeServerToolEmbeddings(name);
2146
- console.log(`Removed tool embeddings for disabled server: ${name}`);
2241
+ logger.log(`Removed tool embeddings for disabled server: ${name}`);
2147
2242
  }
2148
2243
  catch (embeddingError) {
2149
- console.warn('Failed to remove embeddings for server', {
2244
+ logger.warn('Failed to remove embeddings for server', {
2150
2245
  serverName: name,
2151
2246
  error: summarizeErrorForLogging(embeddingError),
2152
2247
  });
@@ -2156,10 +2251,10 @@ export const toggleServerStatus = async (name, enabled) => {
2156
2251
  // If enabling, reconnect the server to restore connection and sync tool embeddings
2157
2252
  try {
2158
2253
  await initializeClientsFromSettings(false, name);
2159
- console.log(`Re-enabled server ${name} and triggered tool embedding sync`);
2254
+ logger.log(`Re-enabled server ${name} and triggered tool embedding sync`);
2160
2255
  }
2161
2256
  catch (reconnectError) {
2162
- console.warn('Failed to reconnect server during enable', {
2257
+ logger.warn('Failed to reconnect server during enable', {
2163
2258
  serverName: name,
2164
2259
  error: summarizeErrorForLogging(reconnectError),
2165
2260
  });
@@ -2168,7 +2263,7 @@ export const toggleServerStatus = async (name, enabled) => {
2168
2263
  return { success: true, message: `Server ${enabled ? 'enabled' : 'disabled'} successfully` };
2169
2264
  }
2170
2265
  catch (error) {
2171
- console.error('Failed to toggle server status', { serverName: name, error });
2266
+ logger.error('Failed to toggle server status', { serverName: name, error });
2172
2267
  return { success: false, message: 'Failed to toggle server status' };
2173
2268
  }
2174
2269
  };
@@ -2310,7 +2405,7 @@ const projectToolForDownstream = (serverName, tool, appsRouteContext, serverConf
2310
2405
  export const handleListToolsRequest = async (_, extra) => {
2311
2406
  const sessionId = extra.sessionId || '';
2312
2407
  const group = getGroup(sessionId);
2313
- console.log(`Handling ListToolsRequest for group: ${group}`);
2408
+ logger.log(`Handling ListToolsRequest for group: ${group}`);
2314
2409
  // Special handling for $smart group to return smart routing tools
2315
2410
  // Support both $smart and $smart/{group} patterns
2316
2411
  if (isSmartRoutingGroup(group)) {
@@ -2322,9 +2417,7 @@ export const handleListToolsRequest = async (_, extra) => {
2322
2417
  // so this list reflects the freshly cached tools instead of returning empty.
2323
2418
  // No wake is triggered from list itself; the prime handles that. See #1029.
2324
2419
  await Promise.allSettled(filteredServerInfos
2325
- .filter((si) => si.config?.startOnDemand === true &&
2326
- si.tools.length === 0 &&
2327
- si.spawningPromise)
2420
+ .filter((si) => si.config?.startOnDemand === true && si.tools.length === 0 && si.spawningPromise)
2328
2421
  .map((si) => si.spawningPromise));
2329
2422
  const allTools = [];
2330
2423
  for (const serverInfo of filteredServerInfos) {
@@ -2354,7 +2447,7 @@ export const handleListToolsRequest = async (_, extra) => {
2354
2447
  };
2355
2448
  };
2356
2449
  export const handleCallToolRequest = async (request, extra) => {
2357
- console.log('Handling CallToolRequest for tool', summarizeToolRequestForLogging(request.params));
2450
+ logger.log('Handling CallToolRequest for tool', summarizeToolRequestForLogging(request.params));
2358
2451
  const startTime = Date.now();
2359
2452
  const activityLogger = getActivityLoggingService();
2360
2453
  // Get request context for activity logging
@@ -2450,7 +2543,7 @@ export const handleCallToolRequest = async (request, extra) => {
2450
2543
  serverInfo.tools.some((tool) => tool.name === toolName));
2451
2544
  }
2452
2545
  if (!targetServerInfo) {
2453
- throw new Error(`No available servers found with tool: ${toolName}`);
2546
+ throw new ToolUnavailableError(`Tool not available: ${toolName}`, classifyUnavailableReason(toolName));
2454
2547
  }
2455
2548
  // If the target is an on-demand server that is not yet running, wake it up now.
2456
2549
  // Concurrent callers are serialised via ensureServerReady's singleton promise.
@@ -2467,7 +2560,7 @@ export const handleCallToolRequest = async (request, extra) => {
2467
2560
  // Check if the tool exists on the server
2468
2561
  const tool = targetTool ?? findToolOnServer(targetServerInfo, targetToolName, singleServerAppsRoute);
2469
2562
  if (!tool) {
2470
- throw new Error(`Tool '${toolName}' not found on server '${targetServerInfo.name}'`);
2563
+ throw new ToolUnavailableError(`Tool not available: ${toolName}`, 'tool-not-found');
2471
2564
  }
2472
2565
  assertToolAvailableForRoute(tool, appsRouteContext);
2473
2566
  // Handle OpenAPI servers differently
@@ -2476,7 +2569,7 @@ export const handleCallToolRequest = async (request, extra) => {
2476
2569
  const openApiClient = targetServerInfo.openApiClient;
2477
2570
  // Use toolArgs if it has properties, otherwise fallback to request.params.arguments
2478
2571
  const finalArgs = toolArgs && typeof toolArgs === 'object' ? toolArgs : {};
2479
- console.log('Invoking OpenAPI tool', {
2572
+ logger.log('Invoking OpenAPI tool', {
2480
2573
  toolName: targetToolName,
2481
2574
  serverName: targetServerInfo.name,
2482
2575
  arguments: summarizeArgumentsForLogging(finalArgs),
@@ -2514,7 +2607,7 @@ export const handleCallToolRequest = async (request, extra) => {
2514
2607
  requestContent: finalArgs,
2515
2608
  responseContent: result,
2516
2609
  });
2517
- console.log('OpenAPI tool invocation result', {
2610
+ logger.log('OpenAPI tool invocation result', {
2518
2611
  serverName: targetServerInfo.name,
2519
2612
  toolName: cleanToolName,
2520
2613
  result: summarizeToolResultForLogging(result),
@@ -2559,7 +2652,7 @@ export const handleCallToolRequest = async (request, extra) => {
2559
2652
  }
2560
2653
  // Use toolArgs if it has properties, otherwise fallback to request.params.arguments
2561
2654
  const finalArgs = toolArgs && typeof toolArgs === 'object' ? toolArgs : {};
2562
- console.log('Invoking tool', {
2655
+ logger.log('Invoking tool', {
2563
2656
  toolName: targetToolName,
2564
2657
  serverName: targetServerInfo.name,
2565
2658
  arguments: summarizeArgumentsForLogging(finalArgs),
@@ -2576,7 +2669,7 @@ export const handleCallToolRequest = async (request, extra) => {
2576
2669
  requestContent: finalArgs,
2577
2670
  responseContent: result,
2578
2671
  });
2579
- console.log('Tool invocation result', {
2672
+ logger.log('Tool invocation result', {
2580
2673
  serverName: targetServerInfo.name,
2581
2674
  toolName: cleanToolName,
2582
2675
  result: summarizeToolResultForLogging(result),
@@ -2619,15 +2712,12 @@ export const handleCallToolRequest = async (request, extra) => {
2619
2712
  : undefined;
2620
2713
  const serverInfo = singleServerAppsRoute
2621
2714
  ? appsRouteContext.serverInfo
2622
- : (groupTool?.serverInfo ??
2623
- (lookupGroup ? undefined : getServerByTool(request.params.name)));
2715
+ : (groupTool?.serverInfo ?? (lookupGroup ? undefined : getServerByTool(request.params.name)));
2624
2716
  const routeToolName = groupTool?.toolName ?? request.params.name;
2625
2717
  const tool = groupTool?.tool ??
2626
- (serverInfo
2627
- ? findToolOnServer(serverInfo, routeToolName, singleServerAppsRoute)
2628
- : undefined);
2718
+ (serverInfo ? findToolOnServer(serverInfo, routeToolName, singleServerAppsRoute) : undefined);
2629
2719
  if (!serverInfo || !tool) {
2630
- throw new Error(`Server not found: ${request.params.name}`);
2720
+ throw new ToolUnavailableError(`Tool not available: ${request.params.name}`, classifyUnavailableReason(request.params.name));
2631
2721
  }
2632
2722
  assertToolAvailableForRoute(tool, appsRouteContext);
2633
2723
  // Wake the on-demand server before invoking the tool. It may be asleep with
@@ -2643,7 +2733,7 @@ export const handleCallToolRequest = async (request, extra) => {
2643
2733
  const openApiClient = serverInfo.openApiClient;
2644
2734
  // Remove server prefix from tool name if present
2645
2735
  const cleanToolName = normalizeToolNameForServer(serverInfo.name, routeToolName);
2646
- console.log('Invoking OpenAPI tool', {
2736
+ logger.log('Invoking OpenAPI tool', {
2647
2737
  toolName: cleanToolName,
2648
2738
  serverName: serverInfo.name,
2649
2739
  arguments: summarizeArgumentsForLogging(request.params.arguments),
@@ -2680,7 +2770,7 @@ export const handleCallToolRequest = async (request, extra) => {
2680
2770
  requestContent: finalArgs,
2681
2771
  responseContent: result,
2682
2772
  });
2683
- console.log('OpenAPI tool invocation result', {
2773
+ logger.log('OpenAPI tool invocation result', {
2684
2774
  serverName: serverInfo.name,
2685
2775
  toolName: cleanToolName,
2686
2776
  result: summarizeToolResultForLogging(result),
@@ -2731,7 +2821,7 @@ export const handleCallToolRequest = async (request, extra) => {
2731
2821
  requestContent: request.params.arguments,
2732
2822
  responseContent: result,
2733
2823
  });
2734
- console.log('Tool call result', {
2824
+ logger.log('Tool call result', {
2735
2825
  serverName: serverInfo.name,
2736
2826
  toolName: cleanToolName,
2737
2827
  result: summarizeToolResultForLogging(result),
@@ -2761,7 +2851,13 @@ export const handleCallToolRequest = async (request, extra) => {
2761
2851
  });
2762
2852
  }
2763
2853
  catch (error) {
2764
- console.error('Error handling CallToolRequest', summarizeErrorForLogging(error));
2854
+ const unavailable = error instanceof ToolUnavailableError
2855
+ ? { message: error.message, reason: error.reason }
2856
+ : undefined;
2857
+ logger.error('Error handling CallToolRequest', {
2858
+ ...summarizeErrorForLogging(error),
2859
+ ...(unavailable ? { reason: unavailable.reason } : {}),
2860
+ });
2765
2861
  // Log error activity
2766
2862
  const duration = Date.now() - startTime;
2767
2863
  await settleHostedIfNeeded({
@@ -2784,9 +2880,15 @@ export const handleCallToolRequest = async (request, extra) => {
2784
2880
  keyId,
2785
2881
  keyName,
2786
2882
  sourceIp,
2787
- errorMessage: formatErrorForLogging(error),
2883
+ // The reason is an internal diagnostic; the caller-facing message stays
2884
+ // identical for hidden and nonexistent targets.
2885
+ errorMessage: unavailable
2886
+ ? `${unavailable.message} (reason: ${unavailable.reason})`
2887
+ : formatErrorForLogging(error),
2788
2888
  });
2789
- const safeErrorText = formatErrorForLogging(error);
2889
+ // For unavailable-target errors, surface exactly the unified message (no
2890
+ // error-class name, no reason) so hidden vs nonexistent is indistinguishable.
2891
+ const safeErrorText = unavailable ? unavailable.message : formatErrorForLogging(error);
2790
2892
  return {
2791
2893
  content: [
2792
2894
  {
@@ -2842,7 +2944,7 @@ export const handleGetPromptRequest = async (request, extra) => {
2842
2944
  serverInfo.prompts.find((prompt) => prompt.name === name));
2843
2945
  }
2844
2946
  if (!server) {
2845
- throw new Error(`Server not found: ${name}`);
2947
+ throw new ToolUnavailableError(`Prompt not available: ${name}`, classifyUnavailableReason(name));
2846
2948
  }
2847
2949
  // Remove server prefix from prompt name if present
2848
2950
  const separator = getNameSeparator();
@@ -2855,20 +2957,27 @@ export const handleGetPromptRequest = async (request, extra) => {
2855
2957
  arguments: promptArgs,
2856
2958
  };
2857
2959
  // Log the final promptParams
2858
- console.log('Calling getPrompt with params', {
2960
+ logger.log('Calling getPrompt with params', {
2859
2961
  name: cleanPromptName || '',
2860
2962
  arguments: summarizeArgumentsForLogging(promptArgs),
2861
2963
  });
2862
2964
  const prompt = await server.client?.getPrompt(promptParams);
2863
- console.log('Received prompt', summarizePromptForLogging(prompt));
2965
+ logger.log('Received prompt', summarizePromptForLogging(prompt));
2864
2966
  if (!prompt) {
2865
2967
  throw new Error(`Prompt not found: ${cleanPromptName}`);
2866
2968
  }
2867
2969
  return prompt;
2868
2970
  }
2869
2971
  catch (error) {
2870
- console.error('Error handling GetPromptRequest', summarizeErrorForLogging(error));
2871
- const safeErrorText = formatErrorForLogging(error);
2972
+ const unavailable = error instanceof ToolUnavailableError
2973
+ ? { message: error.message, reason: error.reason }
2974
+ : undefined;
2975
+ logger.error('Error handling GetPromptRequest', {
2976
+ ...summarizeErrorForLogging(error),
2977
+ ...(unavailable ? { reason: unavailable.reason } : {}),
2978
+ });
2979
+ // Surface exactly the unified message for unavailable targets (see #1103).
2980
+ const safeErrorText = unavailable ? unavailable.message : formatErrorForLogging(error);
2872
2981
  return {
2873
2982
  content: [
2874
2983
  {
@@ -2884,7 +2993,7 @@ export const handleListPromptsRequest = async (_, extra) => {
2884
2993
  const sessionId = extra.sessionId || '';
2885
2994
  const group = getGroup(sessionId);
2886
2995
  const lookupGroup = getGroupLookupName(group);
2887
- console.log(`Handling ListPromptsRequest for group: ${group}`);
2996
+ logger.log(`Handling ListPromptsRequest for group: ${group}`);
2888
2997
  // Start with built-in prompts (only enabled ones)
2889
2998
  const builtinPrompts = await getBuiltinPromptDao().findEnabled();
2890
2999
  const allPrompts = builtinPrompts.map((bp) => normalizePromptForList({
@@ -2928,7 +3037,7 @@ export const handleListResourcesRequest = async (_, extra) => {
2928
3037
  const sessionId = extra.sessionId || '';
2929
3038
  const group = getGroup(sessionId);
2930
3039
  const lookupGroup = getGroupLookupName(group);
2931
- console.log(`Handling ListResourcesRequest for group: ${group}`);
3040
+ logger.log(`Handling ListResourcesRequest for group: ${group}`);
2932
3041
  const appsRouteContext = await getMcpAppsRouteContext(sessionId, group);
2933
3042
  // Start with built-in resources (only enabled ones)
2934
3043
  const builtinResources = await getBuiltinResourceDao().findEnabled();
@@ -2973,7 +3082,7 @@ export const handleListResourceTemplatesRequest = async (_, extra) => {
2973
3082
  const sessionId = extra.sessionId || '';
2974
3083
  const group = getGroup(sessionId);
2975
3084
  const lookupGroup = getGroupLookupName(group);
2976
- console.log(`Handling ListResourceTemplatesRequest for group: ${group}`);
3085
+ logger.log(`Handling ListResourceTemplatesRequest for group: ${group}`);
2977
3086
  const appsRouteContext = await getMcpAppsRouteContext(sessionId, group);
2978
3087
  const { filteredServerInfos, serverConfigsByName } = await getFilteredServerInfosForGroup(lookupGroup, {
2979
3088
  requireClient: true,
@@ -3076,7 +3185,7 @@ export const handleReadResourceRequest = async (request, extra) => {
3076
3185
  };
3077
3186
  }
3078
3187
  catch (error) {
3079
- console.error('Error handling ReadResourceRequest', summarizeErrorForLogging(error));
3188
+ logger.error('Error handling ReadResourceRequest', summarizeErrorForLogging(error));
3080
3189
  const safeErrorText = formatErrorForLogging(error);
3081
3190
  return {
3082
3191
  contents: [