@samanhappy/mcphub 1.1.7 → 1.1.9

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 (140) hide show
  1. package/dist/clients/openapi.js +2 -1
  2. package/dist/clients/openapi.js.map +1 -1
  3. package/dist/controllers/authController.js +27 -1
  4. package/dist/controllers/authController.js.map +1 -1
  5. package/dist/controllers/configController.js +4 -1
  6. package/dist/controllers/configController.js.map +1 -1
  7. package/dist/controllers/oauthServerController.js +12 -14
  8. package/dist/controllers/oauthServerController.js.map +1 -1
  9. package/dist/controllers/promptController.js +2 -2
  10. package/dist/controllers/promptController.js.map +1 -1
  11. package/dist/controllers/serverController.js +201 -12
  12. package/dist/controllers/serverController.js.map +1 -1
  13. package/dist/dao/OAuthTokenDaoDbImpl.js +2 -0
  14. package/dist/dao/OAuthTokenDaoDbImpl.js.map +1 -1
  15. package/dist/dao/ServerDao.js.map +1 -1
  16. package/dist/dao/ServerDaoDbImpl.js.map +1 -1
  17. package/dist/db/entities/OAuthToken.js +4 -0
  18. package/dist/db/entities/OAuthToken.js.map +1 -1
  19. package/dist/db/entities/Server.js.map +1 -1
  20. package/dist/middlewares/auth.js +7 -25
  21. package/dist/middlewares/auth.js.map +1 -1
  22. package/dist/middlewares/userContext.js +6 -34
  23. package/dist/middlewares/userContext.js.map +1 -1
  24. package/dist/models/OAuth.js +14 -93
  25. package/dist/models/OAuth.js.map +1 -1
  26. package/dist/routes/index.js +3 -1
  27. package/dist/routes/index.js.map +1 -1
  28. package/dist/services/betterAuthConfig.js +1 -0
  29. package/dist/services/betterAuthConfig.js.map +1 -1
  30. package/dist/services/contextCostService.js +3 -1
  31. package/dist/services/contextCostService.js.map +1 -1
  32. package/dist/services/credentialBindingService.js +8 -3
  33. package/dist/services/credentialBindingService.js.map +1 -1
  34. package/dist/services/elicitationService.js +93 -0
  35. package/dist/services/elicitationService.js.map +1 -0
  36. package/dist/services/mcpService.js +167 -95
  37. package/dist/services/mcpService.js.map +1 -1
  38. package/dist/services/oauthServerService.js +15 -1
  39. package/dist/services/oauthServerService.js.map +1 -1
  40. package/dist/services/openApiGeneratorService.js +3 -1
  41. package/dist/services/openApiGeneratorService.js.map +1 -1
  42. package/dist/services/personalOAuthService.js +70 -8
  43. package/dist/services/personalOAuthService.js.map +1 -1
  44. package/dist/services/registrationService.js +28 -0
  45. package/dist/services/registrationService.js.map +1 -0
  46. package/dist/services/serverConfigPresenter.js +6 -1
  47. package/dist/services/serverConfigPresenter.js.map +1 -1
  48. package/dist/services/smartRoutingService.js +7 -6
  49. package/dist/services/smartRoutingService.js.map +1 -1
  50. package/dist/services/sseService.js +1 -2
  51. package/dist/services/sseService.js.map +1 -1
  52. package/dist/services/templateService.js +5 -4
  53. package/dist/services/templateService.js.map +1 -1
  54. package/dist/services/userService.js +22 -4
  55. package/dist/services/userService.js.map +1 -1
  56. package/dist/utils/credentialTemplate.js +76 -6
  57. package/dist/utils/credentialTemplate.js.map +1 -1
  58. package/dist/utils/migration.js +1 -0
  59. package/dist/utils/migration.js.map +1 -1
  60. package/dist/utils/namePattern.js +28 -0
  61. package/dist/utils/namePattern.js.map +1 -0
  62. package/dist/utils/oauthBearer.js +4 -2
  63. package/dist/utils/oauthBearer.js.map +1 -1
  64. package/dist/utils/passthroughHeaders.js +17 -0
  65. package/dist/utils/passthroughHeaders.js.map +1 -0
  66. package/dist/utils/rateLimit.js +1 -1
  67. package/dist/utils/rateLimit.js.map +1 -1
  68. package/dist/utils/serverConfigPersistence.js +1 -3
  69. package/dist/utils/serverConfigPersistence.js.map +1 -1
  70. package/dist/utils/serverConfigValidation.js +2 -1
  71. package/dist/utils/serverConfigValidation.js.map +1 -1
  72. package/dist/utils/settingsSync.js +5 -1
  73. package/dist/utils/settingsSync.js.map +1 -1
  74. package/dist/utils/toolConfig.js +93 -0
  75. package/dist/utils/toolConfig.js.map +1 -0
  76. package/dist/utils/upstreamError.js +9 -2
  77. package/dist/utils/upstreamError.js.map +1 -1
  78. package/dist/utils/userSession.js +29 -0
  79. package/dist/utils/userSession.js.map +1 -0
  80. package/frontend/dist/assets/{ActivityPage-BEpHoGLB.js → ActivityPage-CFRX20v4.js} +2 -2
  81. package/frontend/dist/assets/{ActivityPage-BEpHoGLB.js.map → ActivityPage-CFRX20v4.js.map} +1 -1
  82. package/frontend/dist/assets/{CopyClientConfigDialog-B4RDMLQ7.js → CopyClientConfigDialog-CDEiSInj.js} +2 -2
  83. package/frontend/dist/assets/{CopyClientConfigDialog-B4RDMLQ7.js.map → CopyClientConfigDialog-CDEiSInj.js.map} +1 -1
  84. package/frontend/dist/assets/CredentialsPage-BCLHY_fN.js +2 -0
  85. package/frontend/dist/assets/CredentialsPage-BCLHY_fN.js.map +1 -0
  86. package/frontend/dist/assets/{Dashboard-Bp8ZGDp5.js → Dashboard-B3Y6nAFL.js} +2 -2
  87. package/frontend/dist/assets/{Dashboard-Bp8ZGDp5.js.map → Dashboard-B3Y6nAFL.js.map} +1 -1
  88. package/frontend/dist/assets/{EndpointCopy-CH7rfwDL.js → EndpointCopy-BMxT99Ne.js} +2 -2
  89. package/frontend/dist/assets/{EndpointCopy-CH7rfwDL.js.map → EndpointCopy-BMxT99Ne.js.map} +1 -1
  90. package/frontend/dist/assets/GroupsPage-5iCk1LKT.js +33 -0
  91. package/frontend/dist/assets/GroupsPage-5iCk1LKT.js.map +1 -0
  92. package/frontend/dist/assets/LoginPage-BjYzY6RH.js +2 -0
  93. package/frontend/dist/assets/LoginPage-BjYzY6RH.js.map +1 -0
  94. package/frontend/dist/assets/{LogsPage-B4RyT3TS.js → LogsPage-CGcFNKJn.js} +2 -2
  95. package/frontend/dist/assets/{LogsPage-B4RyT3TS.js.map → LogsPage-CGcFNKJn.js.map} +1 -1
  96. package/frontend/dist/assets/{MarketPage-DV7Bs6bj.js → MarketPage-B7JPqFw3.js} +2 -2
  97. package/frontend/dist/assets/{MarketPage-DV7Bs6bj.js.map → MarketPage-B7JPqFw3.js.map} +1 -1
  98. package/frontend/dist/assets/{OAuthConsentPage-DdxMIqDO.js → OAuthConsentPage-BYi_kgNj.js} +2 -2
  99. package/frontend/dist/assets/{OAuthConsentPage-DdxMIqDO.js.map → OAuthConsentPage-BYi_kgNj.js.map} +1 -1
  100. package/frontend/dist/assets/{PromptsPage-61Uj8gCp.js → PromptsPage-x6X5trLX.js} +2 -2
  101. package/frontend/dist/assets/{PromptsPage-61Uj8gCp.js.map → PromptsPage-x6X5trLX.js.map} +1 -1
  102. package/frontend/dist/assets/{ResourcesPage-BSgOwVld.js → ResourcesPage-BJZCPBs8.js} +2 -2
  103. package/frontend/dist/assets/{ResourcesPage-BSgOwVld.js.map → ResourcesPage-BJZCPBs8.js.map} +1 -1
  104. package/frontend/dist/assets/ServersPage-CsyGLden.js +39 -0
  105. package/frontend/dist/assets/ServersPage-CsyGLden.js.map +1 -0
  106. package/frontend/dist/assets/{SettingsPage-C8zYH_jq.js → SettingsPage-C53DI0Sz.js} +6 -6
  107. package/frontend/dist/assets/SettingsPage-C53DI0Sz.js.map +1 -0
  108. package/frontend/dist/assets/{StatusDot-R86yLB-f.js → StatusDot-BDJd3Iix.js} +2 -2
  109. package/frontend/dist/assets/{StatusDot-R86yLB-f.js.map → StatusDot-BDJd3Iix.js.map} +1 -1
  110. package/frontend/dist/assets/{ToggleGroup-DN1ttokb.js → ToggleGroup-BbmRhILg.js} +2 -2
  111. package/frontend/dist/assets/{ToggleGroup-DN1ttokb.js.map → ToggleGroup-BbmRhILg.js.map} +1 -1
  112. package/frontend/dist/assets/{UsersPage-BCMjm9JG.js → UsersPage-NUG6vF8C.js} +2 -2
  113. package/frontend/dist/assets/{UsersPage-BCMjm9JG.js.map → UsersPage-NUG6vF8C.js.map} +1 -1
  114. package/frontend/dist/assets/{clipboard-DjzVWhkB.js → clipboard-7BggsilD.js} +2 -2
  115. package/frontend/dist/assets/{clipboard-DjzVWhkB.js.map → clipboard-7BggsilD.js.map} +1 -1
  116. package/frontend/dist/assets/index-B_IITM9e.js +3 -0
  117. package/frontend/dist/assets/index-B_IITM9e.js.map +1 -0
  118. package/frontend/dist/assets/index-CnqLKewL.css +1 -0
  119. package/frontend/dist/assets/{resourceService-DRZl8_Vm.js → resourceService-B674Fopr.js} +2 -2
  120. package/frontend/dist/assets/{resourceService-DRZl8_Vm.js.map → resourceService-B674Fopr.js.map} +1 -1
  121. package/frontend/dist/assets/{useCostData-DVLGVcuy.js → useCostData-8Z1HjAL5.js} +2 -2
  122. package/frontend/dist/assets/{useCostData-DVLGVcuy.js.map → useCostData-8Z1HjAL5.js.map} +1 -1
  123. package/frontend/dist/assets/variableDetection-C0nP3Jj2.js +18 -0
  124. package/frontend/dist/assets/variableDetection-C0nP3Jj2.js.map +1 -0
  125. package/frontend/dist/index.html +2 -2
  126. package/package.json +2 -2
  127. package/frontend/dist/assets/CredentialsPage-46Z8Qv7O.js +0 -2
  128. package/frontend/dist/assets/CredentialsPage-46Z8Qv7O.js.map +0 -1
  129. package/frontend/dist/assets/GroupsPage-DTjOja6T.js +0 -33
  130. package/frontend/dist/assets/GroupsPage-DTjOja6T.js.map +0 -1
  131. package/frontend/dist/assets/LoginPage-o6ibb_j3.js +0 -2
  132. package/frontend/dist/assets/LoginPage-o6ibb_j3.js.map +0 -1
  133. package/frontend/dist/assets/ServersPage-DYumMhzV.js +0 -39
  134. package/frontend/dist/assets/ServersPage-DYumMhzV.js.map +0 -1
  135. package/frontend/dist/assets/SettingsPage-C8zYH_jq.js.map +0 -1
  136. package/frontend/dist/assets/index--NhF_yyy.css +0 -1
  137. package/frontend/dist/assets/index-P_O2kAcB.js +0 -3
  138. package/frontend/dist/assets/index-P_O2kAcB.js.map +0 -1
  139. package/frontend/dist/assets/variableDetection-DiE4h0o7.js +0 -16
  140. package/frontend/dist/assets/variableDetection-DiE4h0o7.js.map +0 -1
@@ -1,19 +1,22 @@
1
+ import { isSafePassthroughHeader } from '../utils/passthroughHeaders.js';
1
2
  import { observeToolCall, setMetricsServer } from './metricsService.js';
2
3
  import { hasPersonalOAuth, PersonalOAuthContext, personalOAuthContexts, personalAuthorizationRequired, } from './personalOAuthService.js';
3
4
  import { preserveListFreshness, combineListFreshness, remainingListTtl, } from '../utils/listFreshness.js';
4
5
  import { expandServerConfig, getServerEnvironment } from './serverConfigEnvironment.js';
5
6
  import { getMcpRequestGroup } from '../utils/mcpRequestGroup.js';
6
7
  import { LegacyMcpClient } from '../clients/legacyMcpClient.js';
8
+ import { getElicitationCapabilities, installElicitationRelay, isElicitationClientClosed, withElicitation, } from './elicitationService.js';
7
9
  import { LEGACY_PROTOCOL_VERSIONS } from '../utils/mcpProtocol.js';
8
10
  import { AsyncLocalStorage } from 'node:async_hooks';
9
11
  import { createHash } from 'node:crypto';
10
12
  import { canAccessGroupRoute } from '../utils/groupAccess.js';
11
13
  import { canServeToolRequests } from '../utils/serverAvailability.js';
12
14
  import { assertToolCallArgumentsSafe, isToolCallGuardEnabled, ToolCallBlockedError, } from '../middlewares/toolCallGuard.js';
15
+ import { applyHiddenParams, findToolConfig, removeHiddenArguments, } from '../utils/toolConfig.js';
13
16
  import { PrincipalRuntimeService } from './principalRuntimeService.js';
14
17
  import { UserContextService } from './userContextService.js';
15
18
  import { deleteCredentialBindings, missingCredentialError } from './credentialBindingService.js';
16
- import { CredentialBindingError, hasCredentialTemplate, hasPersonalAuthentication, resolveCredentialArguments, validateCredentialTemplate, } from '../utils/credentialTemplate.js';
19
+ import { CredentialBindingError, hasCredentialTemplate, hasPersonalAuthentication, resolveCredentialArguments, hasUrlCredentialSlots, validateCredentialTemplate, } from '../utils/credentialTemplate.js';
17
20
  import os from 'os';
18
21
  import path from 'path';
19
22
  import fs from 'fs';
@@ -353,12 +356,12 @@ const setSessionIsolatedClient = (sessionId, serverName, client, transport) => {
353
356
  * Used when the server has `perSessionClient: true` in its config.
354
357
  * Uses a creation lock to prevent duplicate connections from concurrent calls.
355
358
  */
356
- const getOrCreateIsolatedClient = async (sessionId, serverInfo) => {
359
+ const getOrCreateIsolatedClient = async (sessionId, serverInfo, downstream) => {
357
360
  // Quick check without lock — already exists
358
361
  const sessionClients = sessionIsolatedClients.get(sessionId);
359
362
  if (sessionClients) {
360
363
  const existing = sessionClients.get(serverInfo.name);
361
- if (existing) {
364
+ if (existing && !isElicitationClientClosed(existing.client)) {
362
365
  return existing;
363
366
  }
364
367
  }
@@ -373,7 +376,7 @@ const getOrCreateIsolatedClient = async (sessionId, serverInfo) => {
373
376
  const sessionClientsAfterLock = sessionIsolatedClients.get(sessionId);
374
377
  if (sessionClientsAfterLock) {
375
378
  const existing = sessionClientsAfterLock.get(serverInfo.name);
376
- if (existing) {
379
+ if (existing && !isElicitationClientClosed(existing.client)) {
377
380
  return existing;
378
381
  }
379
382
  }
@@ -386,7 +389,7 @@ const getOrCreateIsolatedClient = async (sessionId, serverInfo) => {
386
389
  }
387
390
  const reservedClients = sessionIsolatedClients.get(sessionId);
388
391
  const transport = await createTransportFromConfig(serverInfo.name, serverConfig);
389
- const client = createUpstreamMcpClient(serverInfo.name, () => serverInfo);
392
+ const client = createUpstreamMcpClient(serverInfo.name, () => serverInfo, downstream);
390
393
  try {
391
394
  await connectClientWithDiagnostics(client, transport, serverInfo.options || {});
392
395
  }
@@ -505,6 +508,8 @@ const hasDescriptionOverride = (config) => {
505
508
  const resolveDescriptionOverride = (defaultDescription, config) => {
506
509
  return hasDescriptionOverride(config) ? config?.description || '' : defaultDescription || '';
507
510
  };
511
+ // The dashboard/API projection keeps the full input schema (the dashboard
512
+ // lists every parameter to choose what to hide) and reports the configured list.
508
513
  const buildToolWithDescriptionMetadata = (tool, toolConfig) => {
509
514
  const upstreamDescription = tool.description || '';
510
515
  return {
@@ -512,13 +517,15 @@ const buildToolWithDescriptionMetadata = (tool, toolConfig) => {
512
517
  description: resolveDescriptionOverride(upstreamDescription, toolConfig),
513
518
  defaultDescription: upstreamDescription,
514
519
  hasDescriptionOverride: hasDescriptionOverride(toolConfig),
520
+ ...(toolConfig?.hiddenParams?.length ? { hiddenParams: toolConfig.hiddenParams } : {}),
515
521
  enabled: toolConfig?.enabled !== false,
516
522
  };
517
523
  };
518
- // Apply any per-tool description override before the tool text is embedded for
519
- // Smart Routing search. Deliberately narrower than buildToolWithDescriptionMetadata:
520
- // only `description` (plus the hasDescriptionOverride marker used by the
521
- // tool-set hash) is touched, so serverInfo.tools (built by
524
+ // Apply any per-tool description override and hidden parameters before the tool
525
+ // text is embedded for Smart Routing search. Deliberately narrower than
526
+ // buildToolWithDescriptionMetadata: only `description` (plus the
527
+ // hasDescriptionOverride marker used by the tool-set hash) and `inputSchema`
528
+ // are touched, so serverInfo.tools (built by
522
529
  // normalizeToolForCache/updateServerToolsCache) still never carries `enabled` --
523
530
  // preserving the existing invariant that execution gating reads the persisted
524
531
  // config directly, not this projection. Without this step, overrides only
@@ -529,16 +536,18 @@ const applyDescriptionOverridesForEmbedding = async (serverName, tools) => {
529
536
  if (!serverConfig?.tools) {
530
537
  return tools;
531
538
  }
539
+ // A partial sync passes only some tools; the key rule needs all of them
540
+ const runtimeNames = (getServerByName(serverName)?.tools ?? tools).map((tool) => tool.name);
532
541
  return tools.map((tool) => {
533
- const bareToolName = normalizeToolNameForServer(serverName, tool.name);
534
- const toolConfig = serverConfig.tools?.[tool.name] ?? serverConfig.tools?.[bareToolName];
542
+ const toolConfig = findToolConfig(serverConfig.tools, serverName, tool.name, runtimeNames);
543
+ const visibleTool = applyHiddenParams(tool, toolConfig);
535
544
  return hasDescriptionOverride(toolConfig)
536
545
  ? {
537
- ...tool,
546
+ ...visibleTool,
538
547
  description: resolveDescriptionOverride(tool.description, toolConfig),
539
548
  hasDescriptionOverride: true,
540
549
  }
541
- : tool;
550
+ : visibleTool;
542
551
  });
543
552
  };
544
553
  // Tail of the embedding tasks queued per server. A per-user connect and a
@@ -832,12 +841,13 @@ const updateServerResourcesCache = (serverInfo, resources) => {
832
841
  const logListChangedRefreshError = (listType) => {
833
842
  logger.warn(`Failed to refresh ${listType} list after upstream notification`);
834
843
  };
835
- const createUpstreamMcpClient = (name, getServerInfo) => {
836
- return new LegacyMcpClient({
844
+ const createUpstreamMcpClient = (name, getServerInfo, downstream) => {
845
+ const elicitation = getElicitationCapabilities(downstream);
846
+ const client = new LegacyMcpClient({
837
847
  name: `mcp-client-${name}`,
838
848
  version: '1.0.0',
839
849
  }, {
840
- capabilities: MCP_APPS_CAPABILITIES,
850
+ capabilities: { ...MCP_APPS_CAPABILITIES, ...(elicitation ? { elicitation } : {}) },
841
851
  jsonSchemaValidator: new ResilientJsonSchemaValidator(),
842
852
  listChanged: {
843
853
  tools: {
@@ -887,6 +897,9 @@ const createUpstreamMcpClient = (name, getServerInfo) => {
887
897
  },
888
898
  },
889
899
  });
900
+ if (elicitation && downstream)
901
+ installElicitationRelay(client, downstream);
902
+ return client;
890
903
  };
891
904
  // Normalize and infer server type for safe client display
892
905
  const normalizeServerType = (type) => {
@@ -1162,7 +1175,7 @@ export const collectPassthroughHeaders = (requestHeaders, passthroughHeaderNames
1162
1175
  const passthroughHeaders = {};
1163
1176
  for (const headerName of passthroughHeaderNames) {
1164
1177
  const normalizedHeaderName = headerName.trim();
1165
- if (!normalizedHeaderName) {
1178
+ if (!isSafePassthroughHeader(normalizedHeaderName)) {
1166
1179
  continue;
1167
1180
  }
1168
1181
  const headerValue = headerValueToString(getHeaderValue(requestHeaders, normalizedHeaderName));
@@ -1221,7 +1234,7 @@ export const createTransportFromConfig = async (name, conf) => {
1221
1234
  // skip the internal-IP blocklist. allowInternal also governs per-hop
1222
1235
  // redirect validation in createRedirectValidatingFetch below.
1223
1236
  const ownerUser = conf.owner ? await getUserDao().findByUsername(conf.owner) : null;
1224
- const allowInternal = !!ownerUser?.isAdmin;
1237
+ const allowInternal = !!ownerUser?.isAdmin && !hasUrlCredentialSlots(conf);
1225
1238
  if (conf.url) {
1226
1239
  await assertSafeUrl(conf.url, { allowInternal });
1227
1240
  }
@@ -1351,8 +1364,18 @@ export const createTransportFromConfig = async (name, conf) => {
1351
1364
  }
1352
1365
  return transport;
1353
1366
  };
1367
+ // Call arguments without the tool's hidden parameters; `toolName` is the bare upstream name.
1368
+ // Looked up by the cached runtime name: findToolConfig removes one server prefix,
1369
+ // so a bare name that itself starts with the prefix (server `fc`, tool
1370
+ // `fc-scrape`) would otherwise resolve to the wrong entry.
1371
+ const withoutHiddenArguments = (serverInfo, toolName, args) => {
1372
+ const runtimeName = `${serverInfo.name}${getNameSeparator()}${toolName}`;
1373
+ const tool = serverInfo.tools?.find((candidate) => candidate.name === runtimeName);
1374
+ const toolConfig = findToolConfig(serverInfo.config?.tools, serverInfo.name, runtimeName, (serverInfo.tools ?? []).map((candidate) => candidate.name));
1375
+ return removeHiddenArguments(args, tool?.inputSchema, toolConfig);
1376
+ };
1354
1377
  // Helper function to handle client.callTool with reconnection logic
1355
- const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries = 1, isolated) => {
1378
+ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries = 1, isolated, extra) => {
1356
1379
  // Local, reassignable refs so a reconnect can swap in the new connection for
1357
1380
  // the next retry attempt. For isolated calls these track the per-session
1358
1381
  // client/transport; otherwise they mirror the shared serverInfo connection.
@@ -1361,17 +1384,33 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
1361
1384
  if (!client) {
1362
1385
  throw new Error(`Client not found for server: ${serverInfo.name}`);
1363
1386
  }
1387
+ // Both MCP call paths pass through here, so hidden parameters are dropped once.
1388
+ toolParams = {
1389
+ ...toolParams,
1390
+ arguments: withoutHiddenArguments(serverInfo, toolParams.name, toolParams.arguments),
1391
+ };
1364
1392
  if (hasPersonalAuthentication(serverInfo.config)) {
1365
1393
  return client.callTool(toolParams, options || {});
1366
1394
  }
1367
1395
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
1368
1396
  try {
1369
- const result = await client.callTool(toolParams, options || {});
1370
- // Check auth error
1371
- checkAuthError(result);
1397
+ const currentClient = client;
1398
+ const result = await withElicitation(currentClient, extra?.mcpReq, options || {}, async (callOptions) => {
1399
+ const result = await currentClient.callTool(toolParams, callOptions);
1400
+ checkAuthError(result);
1401
+ return result;
1402
+ });
1372
1403
  return result;
1373
1404
  }
1374
1405
  catch (error) {
1406
+ if (isElicitationClientClosed(client)) {
1407
+ closeIsolatedClient(serverInfo.name, client, transport);
1408
+ throw error;
1409
+ }
1410
+ // A cancelled queued call never reached the upstream. In particular, do
1411
+ // not let the generic SSE retry replace its healthy isolated connection.
1412
+ if (extra?.mcpReq?.signal.aborted)
1413
+ throw error;
1375
1414
  const isHttp40xError = isRecoverableHttp4xxError(error);
1376
1415
  // Only retry for StreamableHTTPClientTransport and SSE transports
1377
1416
  const isStreamableHttp = transport instanceof StreamableHTTPClientTransport;
@@ -2094,11 +2133,8 @@ export const getServersInfo = async (page, limit, user) => {
2094
2133
  // as the dashboard sends, or bare, as a direct API call may) with no
2095
2134
  // normalization — check both so the dashboard reflects a disable set
2096
2135
  // either way instead of always showing enabled:true for one of them.
2097
- const toolsWithEnabled = tools.map((tool) => {
2098
- const bareToolName = normalizeToolNameForServer(name, tool.name);
2099
- const toolConfig = serverConfig?.tools?.[tool.name] ?? serverConfig?.tools?.[bareToolName];
2100
- return buildToolWithDescriptionMetadata(tool, toolConfig);
2101
- });
2136
+ const runtimeNames = tools.map((tool) => tool.name);
2137
+ const toolsWithEnabled = tools.map((tool) => buildToolWithDescriptionMetadata(tool, findToolConfig(serverConfig?.tools, name, tool.name, runtimeNames)));
2102
2138
  const promptsWithEnabled = prompts.map((prompt) => {
2103
2139
  const promptConfig = serverConfig?.prompts?.[prompt.name];
2104
2140
  return {
@@ -2339,12 +2375,12 @@ const filterToolsByConfig = async (serverName, tools) => {
2339
2375
  // If no tool configuration exists, all tools are enabled by default
2340
2376
  return tools;
2341
2377
  }
2378
+ // Callers may pass a subset; the key rule needs every tool of the server
2379
+ const runtimeNames = (getServerByName(serverName)?.tools ?? tools).map((tool) => tool.name);
2342
2380
  return tools.filter((tool) => {
2343
- // toggleTool stores the disable under whatever key string the caller used
2344
- // (bare upstream name or server-prefixed name, never normalized) — check
2345
- // both, same as findToolOnServer's execution-time gate.
2346
- const bareToolName = normalizeToolNameForServer(serverName, tool.name);
2347
- const toolConfig = serverConfig.tools?.[tool.name] ?? serverConfig.tools?.[bareToolName];
2381
+ // A disable may be stored under the runtime or the bare name — same key rule
2382
+ // as findToolOnServer's execution-time gate (findToolConfig).
2383
+ const toolConfig = findToolConfig(serverConfig.tools, serverName, tool.name, runtimeNames);
2348
2384
  // If tool is not in config, it's enabled by default
2349
2385
  return toolConfig?.enabled !== false;
2350
2386
  });
@@ -2920,24 +2956,25 @@ const getMcpAppsRouteContext = async (sessionId, group) => {
2920
2956
  return eligible.length > 0 ? { enabled: true, serverInfos: eligible } : { enabled: false };
2921
2957
  }
2922
2958
  const { filteredServerInfos, serverConfigsByName } = await getFilteredServerInfosForGroup(group);
2923
- const connectedServerInfos = filteredServerInfos.filter((serverInfo) => serverInfo.status === 'connected' && !!serverInfo.client);
2924
- if (connectedServerInfos.length === 0) {
2959
+ const eligibleServerInfos = filteredServerInfos.filter((serverInfo) => canServeToolRequests(serverInfo) &&
2960
+ (!!serverInfo.client || serverInfo.config?.startOnDemand === true));
2961
+ if (eligibleServerInfos.length === 0) {
2925
2962
  return { enabled: false };
2926
2963
  }
2927
2964
  // Connection availability must not turn an aggregate route into a raw-name
2928
2965
  // route: OpenAPI uses openApiClient, and tool calls acquire only the target
2929
2966
  // personal runtime. Match resolveToolInGroup's configured-route ambiguity check.
2930
- if (connectedServerInfos.length === 1 &&
2967
+ if (eligibleServerInfos.length === 1 &&
2931
2968
  filteredServerInfos.length === 1 &&
2932
2969
  serverConfigsByName.size <= 1) {
2933
2970
  return {
2934
2971
  enabled: true,
2935
- serverInfo: connectedServerInfos[0],
2972
+ serverInfo: eligibleServerInfos[0],
2936
2973
  };
2937
2974
  }
2938
2975
  return {
2939
2976
  enabled: true,
2940
- serverInfos: connectedServerInfos,
2977
+ serverInfos: eligibleServerInfos,
2941
2978
  };
2942
2979
  };
2943
2980
  const normalizeToolNameForServer = (serverName, toolName) => {
@@ -2972,9 +3009,14 @@ const projectNameForGroup = (name, serverName, serverConfig) => {
2972
3009
  const resolveNameFromGroup = (name, serverName, serverConfig) => {
2973
3010
  return replacePrefixedServerName(name, getExposedServerName(serverName, serverConfig), serverName);
2974
3011
  };
2975
- const findToolOnServer = (serverInfo, toolName, allowRawName) => {
2976
- const tool = serverInfo.tools.find((tool) => tool.name === toolName ||
2977
- (allowRawName && normalizeToolNameForServer(serverInfo.name, tool.name) === toolName));
3012
+ const findToolOnServer = (serverInfo, toolName, allowRawName, preferRawName = false) => {
3013
+ // A listed upstream name takes precedence over a colliding legacy cache alias.
3014
+ const rawTool = preferRawName && allowRawName
3015
+ ? serverInfo.tools.find((tool) => normalizeToolNameForServer(serverInfo.name, tool.name) === toolName)
3016
+ : undefined;
3017
+ const tool = rawTool ||
3018
+ serverInfo.tools.find((tool) => tool.name === toolName ||
3019
+ (allowRawName && normalizeToolNameForServer(serverInfo.name, tool.name) === toolName));
2978
3020
  if (!tool) {
2979
3021
  return undefined;
2980
3022
  }
@@ -2989,16 +3031,11 @@ const findToolOnServer = (serverInfo, toolName, allowRawName) => {
2989
3031
  // which is a different object. So the persisted per-tool config must be
2990
3032
  // consulted directly here.
2991
3033
  //
2992
- // toggleTool (serverController.ts) stores `tools[toolName] = ...` using
2993
- // whatever string the caller passed as :toolName, with no normalization.
2994
- // The dashboard sends the already-prefixed `tool.name`, matching what
2995
- // getServersInfo's toolsWithEnabled already reads — but any other caller
2996
- // (REST API used directly, scripts, docs examples) can just as validly
2997
- // pass the bare upstream name, which then never matches a prefixed-only
2998
- // lookup. Check both so a disable takes effect regardless of which form
2999
- // was used to set it.
3000
- const bareToolName = normalizeToolNameForServer(serverInfo.name, tool.name);
3001
- const toolConfig = serverInfo.config?.tools?.[tool.name] ?? serverInfo.config?.tools?.[bareToolName];
3034
+ // A disable may be stored under the runtime (prefixed) name, as the
3035
+ // dashboard writes it, or under the bare upstream name, as a settings file
3036
+ // or a direct API call may. findToolConfig checks both, and never reads
3037
+ // another tool's runtime key as this tool's bare name.
3038
+ const toolConfig = findToolConfig(serverInfo.config?.tools, serverInfo.name, tool.name, serverInfo.tools.map((candidate) => candidate.name));
3002
3039
  if (toolConfig && toolConfig.enabled === false) {
3003
3040
  return undefined;
3004
3041
  }
@@ -3028,7 +3065,7 @@ const resolveToolInGroup = async (group, toolName) => {
3028
3065
  }
3029
3066
  const serverConfig = serverConfigsByName.get(serverInfo.name);
3030
3067
  const internalToolName = resolveNameFromGroup(toolName, serverInfo.name, serverConfig);
3031
- const tool = findToolOnServer(serverInfo, internalToolName, allowRawName);
3068
+ const tool = findToolOnServer(serverInfo, internalToolName, allowRawName, serverConfigsByName.size === 0);
3032
3069
  if (!tool) {
3033
3070
  continue;
3034
3071
  }
@@ -3040,6 +3077,17 @@ const resolveToolInGroup = async (group, toolName) => {
3040
3077
  }
3041
3078
  return undefined;
3042
3079
  };
3080
+ const findEnabledPromptOnServer = async (serverInfo, name, allowRawName = false) => {
3081
+ if (serverInfo.status !== 'connected' || serverInfo.enabled === false)
3082
+ return undefined;
3083
+ const prompt = serverInfo.prompts.find((item) => item.name === name ||
3084
+ (allowRawName && item.name === `${serverInfo.name}${getNameSeparator()}${name}`));
3085
+ if (!prompt)
3086
+ return undefined;
3087
+ const config = await getServerDao().findById(serverInfo.name);
3088
+ const promptConfig = findToolConfig(config?.prompts, serverInfo.name, prompt.name, serverInfo.prompts.map((item) => item.name));
3089
+ return config?.enabled === false || promptConfig?.enabled === false ? undefined : prompt;
3090
+ };
3043
3091
  async function resolvePromptInGroup(group, promptName) {
3044
3092
  const lookupGroup = getGroupLookupName(group);
3045
3093
  if (!lookupGroup) {
@@ -3052,7 +3100,7 @@ async function resolvePromptInGroup(group, promptName) {
3052
3100
  }
3053
3101
  const serverConfig = serverConfigsByName.get(serverInfo.name);
3054
3102
  const internalPromptName = resolveNameFromGroup(promptName, serverInfo.name, serverConfig);
3055
- const prompt = serverInfo.prompts.find((item) => item.name === internalPromptName);
3103
+ const prompt = await findEnabledPromptOnServer(serverInfo, internalPromptName);
3056
3104
  if (!prompt) {
3057
3105
  continue;
3058
3106
  }
@@ -3064,21 +3112,19 @@ async function resolvePromptInGroup(group, promptName) {
3064
3112
  }
3065
3113
  return undefined;
3066
3114
  }
3067
- const projectToolForDownstream = (serverName, tool, appsRouteContext, serverConfig) => {
3115
+ const projectToolForDownstream = (serverName, tool, appsRouteContext, serverConfig, useRawName = false) => {
3068
3116
  if (!appsRouteContext.enabled && isAppOnlyTool(tool)) {
3069
3117
  return undefined;
3070
3118
  }
3071
3119
  const projectedTool = appsRouteContext.enabled ? tool : stripMcpAppsMetadata(tool);
3072
- // Single configured server: keep the existing raw (unqualified) name so
3073
- // widgets that call back with the bare tool name continue to work.
3074
- if (appsRouteContext.serverInfo) {
3120
+ // Naming follows the route, independently of Apps capabilities or connection state.
3121
+ if (useRawName) {
3075
3122
  return {
3076
3123
  ...projectedTool,
3077
3124
  name: normalizeToolNameForServer(serverName, projectedTool.name),
3078
3125
  };
3079
3126
  }
3080
- // Aggregate routes (Apps-enabled) or Apps disabled: qualify the
3081
- // name so it can still be routed to the right server unambiguously.
3127
+ // Group and aggregate routes keep qualified names, including single-member groups.
3082
3128
  return {
3083
3129
  ...projectedTool,
3084
3130
  name: projectNameForGroup(projectedTool.name, serverName, serverConfig),
@@ -3091,6 +3137,7 @@ const listGroupTools = async (group, filteredServerInfos, serverConfigsByName, a
3091
3137
  await Promise.allSettled(filteredServerInfos
3092
3138
  .filter((si) => si.config?.startOnDemand === true && si.tools.length === 0 && si.spawningPromise)
3093
3139
  .map((si) => si.spawningPromise));
3140
+ const useRawName = !!group && !isSmartRoutingGroup(group) && !(await findRouteGroup(group));
3094
3141
  const allTools = [];
3095
3142
  for (const serverInfo of filteredServerInfos) {
3096
3143
  if (serverInfo.tools && serverInfo.tools.length > 0) {
@@ -3099,17 +3146,18 @@ const listGroupTools = async (group, filteredServerInfos, serverConfigsByName, a
3099
3146
  let tools = await filterToolsByConfig(serverInfo.name, serverInfo.tools);
3100
3147
  // If this is a group request, apply group-level tool filtering
3101
3148
  tools = await filterToolsByGroup(group, serverInfo.name, tools, groupServerConfig);
3102
- // Apply custom descriptions from server configuration
3149
+ // Apply custom descriptions and hidden parameters from server configuration
3103
3150
  const serverConfig = await getServerDao().findById(serverInfo.name);
3151
+ const runtimeNames = serverInfo.tools.map((tool) => tool.name);
3104
3152
  const toolsWithCustomDescriptions = tools.map((tool) => {
3105
- const toolConfig = serverConfig?.tools?.[tool.name];
3153
+ const toolConfig = findToolConfig(serverConfig?.tools, serverInfo.name, tool.name, runtimeNames);
3106
3154
  return {
3107
- ...tool,
3155
+ ...applyHiddenParams(tool, toolConfig),
3108
3156
  description: resolveDescriptionOverride(tool.description, toolConfig),
3109
3157
  };
3110
3158
  });
3111
3159
  allTools.push(...toolsWithCustomDescriptions.flatMap((tool) => {
3112
- const projectedTool = projectToolForDownstream(serverInfo.name, tool, appsRouteContext, groupServerConfig);
3160
+ const projectedTool = projectToolForDownstream(serverInfo.name, tool, appsRouteContext, groupServerConfig, useRawName);
3113
3161
  return projectedTool ? [projectedTool] : [];
3114
3162
  }));
3115
3163
  }
@@ -3266,28 +3314,33 @@ const handleCallToolRequestImpl = async (request, extra) => {
3266
3314
  };
3267
3315
  try {
3268
3316
  appsRouteContext = await getMcpAppsRouteContext(sessionId, group);
3317
+ const lookupRoute = getGroupLookupName(group);
3318
+ // Direct routes own their listed raw names, even when a tool is disabled.
3319
+ const upstreamOwnsMetaName = SMART_ROUTING_META_TOOL_NAMES.has(request.params.name) &&
3320
+ !!lookupRoute &&
3321
+ !isSmartRoutingGroup(group) &&
3322
+ !(await findRouteGroup(lookupRoute)) &&
3323
+ getVisibleServerByName(lookupRoute)?.tools.some((tool) => normalizeToolNameForServer(lookupRoute, tool.name) === request.params.name);
3269
3324
  // Special handling for smart routing tools
3270
- if (request.params.name === 'search_tools') {
3325
+ if (!upstreamOwnsMetaName && request.params.name === 'search_tools') {
3271
3326
  const { query, limit = 10 } = request.params.arguments || {};
3272
3327
  return await handleSearchToolsRequest(query, limit, sessionId, group);
3273
3328
  }
3274
3329
  // Special handling for describe_tool (progressive disclosure mode)
3275
- if (request.params.name === 'describe_tool') {
3330
+ if (!upstreamOwnsMetaName && request.params.name === 'describe_tool') {
3276
3331
  const { toolName } = request.params.arguments || {};
3277
3332
  return await handleDescribeToolRequest(toolName, sessionId, group);
3278
3333
  }
3279
3334
  // Special handling for call_tool
3280
- if (request.params.name === 'call_tool') {
3335
+ if (!upstreamOwnsMetaName && request.params.name === 'call_tool') {
3281
3336
  const { toolName } = request.params.arguments || {};
3282
3337
  if (!toolName) {
3283
3338
  throw new Error('toolName parameter is required');
3284
3339
  }
3285
3340
  const { arguments: toolArgs } = request.params.arguments || {};
3286
- // A single connected server on an Apps-enabled route is addressed by
3287
- // its raw (unqualified) tool name, same as before. A multi-server
3288
- // Apps-enabled route falls through to qualified-name group
3289
- // resolution below, same as an ordinary (non-Apps) group route.
3290
- const singleServerAppsRoute = !!appsRouteContext.serverInfo;
3341
+ // Preserve legacy raw aggregate calls; explicit routes use the shared
3342
+ // resolver so cached names still obey group filtering.
3343
+ const singleServerAppsRoute = !getGroupLookupName(group) && !!appsRouteContext.serverInfo;
3291
3344
  let targetServerInfo;
3292
3345
  let targetToolName = toolName;
3293
3346
  let targetTool;
@@ -3348,7 +3401,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
3348
3401
  arguments: summarizeArgumentsForLogging(finalArgs),
3349
3402
  });
3350
3403
  // Remove server prefix from tool name if present
3351
- const cleanToolName = normalizeToolNameForServer(targetServerInfo.name, targetToolName);
3404
+ const cleanToolName = normalizeToolNameForServer(targetServerInfo.name, tool.name);
3352
3405
  // Extract passthrough headers from extra or request context
3353
3406
  let passthroughHeaders;
3354
3407
  let requestHeaders = null;
@@ -3364,6 +3417,8 @@ const handleCallToolRequestImpl = async (request, extra) => {
3364
3417
  if (requestHeaders && targetServerInfo.config?.openapi?.passthroughHeaders) {
3365
3418
  passthroughHeaders = {};
3366
3419
  for (const headerName of targetServerInfo.config.openapi.passthroughHeaders) {
3420
+ if (!isSafePassthroughHeader(headerName))
3421
+ continue;
3367
3422
  // Handle different header name cases (Express normalizes headers to lowercase)
3368
3423
  const headerValue = requestHeaders[headerName] || requestHeaders[headerName.toLowerCase()];
3369
3424
  if (headerValue) {
@@ -3380,7 +3435,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
3380
3435
  throw new Error(`OpenAPI server '${targetServerInfo.name}' requires cookie-session state and is not yet available on stateless MCP requests`);
3381
3436
  }
3382
3437
  await reserveHostedIfNeeded(targetServerInfo.name, cleanToolName);
3383
- const result = await openApiClient.callTool(cleanToolName, finalArgs, passthroughHeaders, false, stateId);
3438
+ const result = await openApiClient.callTool(cleanToolName, withoutHiddenArguments(targetServerInfo, cleanToolName, finalArgs), passthroughHeaders, false, stateId);
3384
3439
  await settleHostedIfNeeded({
3385
3440
  success: true,
3386
3441
  requestContent: finalArgs,
@@ -3429,7 +3484,9 @@ const handleCallToolRequestImpl = async (request, extra) => {
3429
3484
  if (!stateId) {
3430
3485
  throw new Error(`Server '${targetServerInfo.name}' requires per-session client isolation and is not yet available on stateless MCP requests`);
3431
3486
  }
3432
- const isolated = await getOrCreateIsolatedClient(stateId, targetServerInfo);
3487
+ const isolated = await getOrCreateIsolatedClient(stateId, targetServerInfo, extra.mcpReq && !requestContextService.getRequestContext()?.stateless
3488
+ ? servers[sessionId]
3489
+ : undefined);
3433
3490
  isolatedCtx = {
3434
3491
  sessionId: stateId,
3435
3492
  client: isolated.client,
@@ -3447,12 +3504,12 @@ const handleCallToolRequestImpl = async (request, extra) => {
3447
3504
  arguments: summarizeArgumentsForLogging(finalArgs),
3448
3505
  perSessionClient: !!targetServerInfo.config?.perSessionClient,
3449
3506
  });
3450
- const cleanToolName = normalizeToolNameForServer(targetServerInfo.name, targetToolName);
3507
+ const cleanToolName = normalizeToolNameForServer(targetServerInfo.name, tool.name);
3451
3508
  await reserveHostedIfNeeded(targetServerInfo.name, cleanToolName);
3452
3509
  const result = await callToolWithReconnect(targetServerInfo, {
3453
3510
  name: cleanToolName,
3454
3511
  arguments: finalArgs,
3455
- }, targetServerInfo.options || {}, 1, isolatedCtx);
3512
+ }, targetServerInfo.options || {}, 1, isolatedCtx, extra);
3456
3513
  await settleHostedIfNeeded({
3457
3514
  success: !result.isError,
3458
3515
  requestContent: finalArgs,
@@ -3488,13 +3545,9 @@ const handleCallToolRequestImpl = async (request, extra) => {
3488
3545
  }
3489
3546
  // Regular tool handling
3490
3547
  const lookupGroup = getGroupLookupName(group);
3491
- // A single connected server on an Apps-enabled route is addressed
3492
- // directly, using its raw (unqualified) tool name, same as before. A
3493
- // multi-server Apps-enabled route resolves via the qualified-name
3494
- // group lookup below, same as an ordinary (non-Apps) group route,
3495
- // just without stripping Apps metadata or hiding app-only tools
3496
- // (assertToolAvailableForRoute below still gates on appsRouteContext.enabled).
3497
- const singleServerAppsRoute = !!appsRouteContext.serverInfo;
3548
+ // Explicit routes share name resolution and group filtering regardless of
3549
+ // Apps support; keep legacy raw aggregate calls compatible.
3550
+ const singleServerAppsRoute = !getGroupLookupName(group) && !!appsRouteContext.serverInfo;
3498
3551
  const groupTool = !singleServerAppsRoute && lookupGroup
3499
3552
  ? await resolveToolInGroup(group, request.params.name)
3500
3553
  : undefined;
@@ -3522,7 +3575,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
3522
3575
  // For OpenAPI servers, use the OpenAPI client
3523
3576
  const openApiClient = serverInfo.openApiClient;
3524
3577
  // Remove server prefix from tool name if present
3525
- const cleanToolName = normalizeToolNameForServer(serverInfo.name, routeToolName);
3578
+ const cleanToolName = normalizeToolNameForServer(serverInfo.name, tool.name);
3526
3579
  logger.log('Invoking OpenAPI tool', {
3527
3580
  toolName: cleanToolName,
3528
3581
  serverName: serverInfo.name,
@@ -3543,6 +3596,8 @@ const handleCallToolRequestImpl = async (request, extra) => {
3543
3596
  if (requestHeaders && serverInfo.config?.openapi?.passthroughHeaders) {
3544
3597
  passthroughHeaders = {};
3545
3598
  for (const headerName of serverInfo.config.openapi.passthroughHeaders) {
3599
+ if (!isSafePassthroughHeader(headerName))
3600
+ continue;
3546
3601
  // Handle different header name cases (Express normalizes headers to lowercase)
3547
3602
  const headerValue = requestHeaders[headerName] || requestHeaders[headerName.toLowerCase()];
3548
3603
  if (headerValue) {
@@ -3560,7 +3615,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
3560
3615
  throw new Error(`OpenAPI server '${serverInfo.name}' requires cookie-session state and is not yet available on stateless MCP requests`);
3561
3616
  }
3562
3617
  await reserveHostedIfNeeded(serverInfo.name, cleanToolName);
3563
- const result = await openApiClient.callTool(cleanToolName, finalArgs, passthroughHeaders, false, stateId);
3618
+ const result = await openApiClient.callTool(cleanToolName, withoutHiddenArguments(serverInfo, cleanToolName, finalArgs), passthroughHeaders, false, stateId);
3564
3619
  await settleHostedIfNeeded({
3565
3620
  success: true,
3566
3621
  requestContent: finalArgs,
@@ -3608,15 +3663,17 @@ const handleCallToolRequestImpl = async (request, extra) => {
3608
3663
  if (!stateId) {
3609
3664
  throw new Error(`Server '${serverInfo.name}' requires per-session client isolation and is not yet available on stateless MCP requests`);
3610
3665
  }
3611
- const isolated = await getOrCreateIsolatedClient(stateId, serverInfo);
3666
+ const isolated = await getOrCreateIsolatedClient(stateId, serverInfo, extra.mcpReq && !requestContextService.getRequestContext()?.stateless
3667
+ ? servers[sessionId]
3668
+ : undefined);
3612
3669
  isolatedCtx = { sessionId: stateId, client: isolated.client, transport: isolated.transport };
3613
3670
  }
3614
3671
  else if (!serverInfo.client) {
3615
3672
  throw new Error(`Client not found for server: ${serverInfo.name}`);
3616
3673
  }
3617
- const cleanToolName = normalizeToolNameForServer(serverInfo.name, routeToolName);
3674
+ const cleanToolName = normalizeToolNameForServer(serverInfo.name, tool.name);
3618
3675
  await reserveHostedIfNeeded(serverInfo.name, cleanToolName);
3619
- const result = await callToolWithReconnect(serverInfo, { ...request.params, name: cleanToolName }, serverInfo.options || {}, 1, isolatedCtx);
3676
+ const result = await callToolWithReconnect(serverInfo, { ...request.params, name: cleanToolName }, serverInfo.options || {}, 1, isolatedCtx, extra);
3620
3677
  await settleHostedIfNeeded({
3621
3678
  success: !result.isError,
3622
3679
  requestContent: request.params.arguments,
@@ -3715,7 +3772,7 @@ const handleGetPromptRequestImpl = async (request, extra) => {
3715
3772
  const { name, arguments: promptArgs } = request.params;
3716
3773
  const group = getMcpRequestGroup(extra);
3717
3774
  // Check built-in prompts first
3718
- const builtinPrompt = await getBuiltinPromptDao().findByName(name);
3775
+ const builtinPrompt = !extra?.server && (await getBuiltinPromptDao().findByName(name));
3719
3776
  if (builtinPrompt && builtinPrompt.enabled !== false) {
3720
3777
  // Perform {{param}} template substitution
3721
3778
  let content = builtinPrompt.template;
@@ -3737,7 +3794,12 @@ const handleGetPromptRequestImpl = async (request, extra) => {
3737
3794
  let promptNameForServer = name;
3738
3795
  const lookupGroup = getGroupLookupName(group);
3739
3796
  if (extra && extra.server) {
3740
- server = getVisibleServerByName(extra.server);
3797
+ const candidate = getVisibleServerByName(extra.server);
3798
+ const prompt = candidate && (await findEnabledPromptOnServer(candidate, name, true));
3799
+ if (prompt) {
3800
+ server = candidate;
3801
+ promptNameForServer = prompt.name;
3802
+ }
3741
3803
  }
3742
3804
  else if (lookupGroup) {
3743
3805
  const groupPrompt = await resolvePromptInGroup(lookupGroup, name);
@@ -3747,10 +3809,14 @@ const handleGetPromptRequestImpl = async (request, extra) => {
3747
3809
  }
3748
3810
  }
3749
3811
  else {
3750
- // Find the first server that has this prompt
3751
- server = getVisibleServerInfos().find((serverInfo) => serverInfo.status === 'connected' &&
3752
- serverInfo.enabled !== false &&
3753
- serverInfo.prompts.find((prompt) => prompt.name === name));
3812
+ for (const candidate of getVisibleServerInfos()) {
3813
+ const prompt = await findEnabledPromptOnServer(candidate, name);
3814
+ if (prompt) {
3815
+ server = candidate;
3816
+ promptNameForServer = prompt.name;
3817
+ break;
3818
+ }
3819
+ }
3754
3820
  }
3755
3821
  if (!server) {
3756
3822
  throw new ToolUnavailableError(`Prompt not available: ${name}`, classifyUnavailableReason(name));
@@ -3819,7 +3885,7 @@ const handleListPromptsRequestImpl = async (_, extra) => {
3819
3885
  let enabledPrompts = serverInfo.prompts;
3820
3886
  if (serverConfig && serverConfig.prompts) {
3821
3887
  enabledPrompts = serverInfo.prompts.filter((prompt) => {
3822
- const promptConfig = serverConfig.prompts?.[prompt.name];
3888
+ const promptConfig = findToolConfig(serverConfig.prompts, serverInfo.name, prompt.name, serverInfo.prompts.map((item) => item.name));
3823
3889
  // If prompt is not in config, it's enabled by default
3824
3890
  return promptConfig?.enabled !== false;
3825
3891
  });
@@ -3986,6 +4052,12 @@ const handleReadResourceRequestImpl = async (request, extra) => {
3986
4052
  ? [appsRouteContext.serverInfo]
3987
4053
  : (appsRouteContext.serverInfos ?? []);
3988
4054
  for (const candidate of candidates) {
4055
+ const config = await getServerDao().findById(candidate.name);
4056
+ if (config?.resources?.[uri]?.enabled === false)
4057
+ continue;
4058
+ const allowed = await filterResourcesByGroup(lookupGroup, candidate.name, [{ uri }], serverConfigsByName.get(candidate.name));
4059
+ if (allowed.length === 0)
4060
+ continue;
3989
4061
  // An on-demand candidate may be idle yet still advertise its cached widget
3990
4062
  // link, so a host can read the ui:// resource before any tool call has
3991
4063
  // woken it. Hold off its idle shutdown for every read, awake or not, so an
@@ -4181,7 +4253,7 @@ const withPrincipalServers = (handler, operation) => async (request, extra) => {
4181
4253
  if (!serverInfos.some((info) => hasPersonalAuthentication(info.config)))
4182
4254
  return handler(request, extra);
4183
4255
  // Built-in content does not require an upstream credential.
4184
- if (operation === 'prompt') {
4256
+ if (operation === 'prompt' && !extra?.server) {
4185
4257
  const builtin = await getBuiltinPromptDao().findByName(request.params?.name);
4186
4258
  if (builtin && builtin.enabled !== false)
4187
4259
  return handler(request, extra);