@samanhappy/mcphub 1.1.8 → 1.1.10

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 (83) hide show
  1. package/dist/controllers/promptController.js +2 -2
  2. package/dist/controllers/promptController.js.map +1 -1
  3. package/dist/controllers/serverController.js +84 -1
  4. package/dist/controllers/serverController.js.map +1 -1
  5. package/dist/routes/index.js +2 -1
  6. package/dist/routes/index.js.map +1 -1
  7. package/dist/services/credentialBindingService.js +8 -3
  8. package/dist/services/credentialBindingService.js.map +1 -1
  9. package/dist/services/elicitationService.js +93 -0
  10. package/dist/services/elicitationService.js.map +1 -0
  11. package/dist/services/mcpService.js +117 -62
  12. package/dist/services/mcpService.js.map +1 -1
  13. package/dist/services/serverConfigPresenter.js +6 -1
  14. package/dist/services/serverConfigPresenter.js.map +1 -1
  15. package/dist/services/sseService.js +1 -2
  16. package/dist/services/sseService.js.map +1 -1
  17. package/dist/services/templateService.js +2 -3
  18. package/dist/services/templateService.js.map +1 -1
  19. package/dist/utils/credentialTemplate.js +76 -6
  20. package/dist/utils/credentialTemplate.js.map +1 -1
  21. package/dist/utils/namePattern.js +28 -0
  22. package/dist/utils/namePattern.js.map +1 -0
  23. package/dist/utils/serverConfigPersistence.js +1 -3
  24. package/dist/utils/serverConfigPersistence.js.map +1 -1
  25. package/dist/utils/upstreamError.js +9 -2
  26. package/dist/utils/upstreamError.js.map +1 -1
  27. package/frontend/dist/assets/{ActivityPage-CJdAfOUF.js → ActivityPage-DDa3yAck.js} +2 -2
  28. package/frontend/dist/assets/{ActivityPage-CJdAfOUF.js.map → ActivityPage-DDa3yAck.js.map} +1 -1
  29. package/frontend/dist/assets/{CopyClientConfigDialog-DKktm8jj.js → CopyClientConfigDialog-DPQOggV-.js} +2 -2
  30. package/frontend/dist/assets/{CopyClientConfigDialog-DKktm8jj.js.map → CopyClientConfigDialog-DPQOggV-.js.map} +1 -1
  31. package/frontend/dist/assets/CredentialsPage-DmxY-iXb.js +2 -0
  32. package/frontend/dist/assets/CredentialsPage-DmxY-iXb.js.map +1 -0
  33. package/frontend/dist/assets/{Dashboard-DM_IiBIT.js → Dashboard-DCvveQBj.js} +2 -2
  34. package/frontend/dist/assets/{Dashboard-DM_IiBIT.js.map → Dashboard-DCvveQBj.js.map} +1 -1
  35. package/frontend/dist/assets/{EndpointCopy-DHQjJesJ.js → EndpointCopy-DRWz8guH.js} +2 -2
  36. package/frontend/dist/assets/{EndpointCopy-DHQjJesJ.js.map → EndpointCopy-DRWz8guH.js.map} +1 -1
  37. package/frontend/dist/assets/GroupsPage-DtoTWqWU.js +33 -0
  38. package/frontend/dist/assets/GroupsPage-DtoTWqWU.js.map +1 -0
  39. package/frontend/dist/assets/{LoginPage-CKrI0By4.js → LoginPage-BVBf8nr6.js} +2 -2
  40. package/frontend/dist/assets/{LoginPage-CKrI0By4.js.map → LoginPage-BVBf8nr6.js.map} +1 -1
  41. package/frontend/dist/assets/{LogsPage-Dh8gRpEH.js → LogsPage-DbsmNJcA.js} +2 -2
  42. package/frontend/dist/assets/{LogsPage-Dh8gRpEH.js.map → LogsPage-DbsmNJcA.js.map} +1 -1
  43. package/frontend/dist/assets/{MarketPage-BN4qL8L9.js → MarketPage-BOggDLJb.js} +2 -2
  44. package/frontend/dist/assets/{MarketPage-BN4qL8L9.js.map → MarketPage-BOggDLJb.js.map} +1 -1
  45. package/frontend/dist/assets/{OAuthConsentPage-Cyj-rrA4.js → OAuthConsentPage-VON__2op.js} +2 -2
  46. package/frontend/dist/assets/{OAuthConsentPage-Cyj-rrA4.js.map → OAuthConsentPage-VON__2op.js.map} +1 -1
  47. package/frontend/dist/assets/{PromptsPage-YWD04XeI.js → PromptsPage-BNkvIDZU.js} +2 -2
  48. package/frontend/dist/assets/{PromptsPage-YWD04XeI.js.map → PromptsPage-BNkvIDZU.js.map} +1 -1
  49. package/frontend/dist/assets/{ResourcesPage-CVRzHCf4.js → ResourcesPage-4WBvamN_.js} +2 -2
  50. package/frontend/dist/assets/{ResourcesPage-CVRzHCf4.js.map → ResourcesPage-4WBvamN_.js.map} +1 -1
  51. package/frontend/dist/assets/{ServersPage-DJBurfOe.js → ServersPage-C7000Szj.js} +7 -7
  52. package/frontend/dist/assets/ServersPage-C7000Szj.js.map +1 -0
  53. package/frontend/dist/assets/{SettingsPage-3sK6NJRn.js → SettingsPage-YX6dJjrM.js} +2 -2
  54. package/frontend/dist/assets/{SettingsPage-3sK6NJRn.js.map → SettingsPage-YX6dJjrM.js.map} +1 -1
  55. package/frontend/dist/assets/{StatusDot-CLZidCyq.js → StatusDot-mgJveZ2h.js} +2 -2
  56. package/frontend/dist/assets/{StatusDot-CLZidCyq.js.map → StatusDot-mgJveZ2h.js.map} +1 -1
  57. package/frontend/dist/assets/{ToggleGroup-Dfh4jKpP.js → ToggleGroup-DXyuwPy7.js} +2 -2
  58. package/frontend/dist/assets/{ToggleGroup-Dfh4jKpP.js.map → ToggleGroup-DXyuwPy7.js.map} +1 -1
  59. package/frontend/dist/assets/{UsersPage-DfFqSRVb.js → UsersPage-BiUn8c0z.js} +2 -2
  60. package/frontend/dist/assets/{UsersPage-DfFqSRVb.js.map → UsersPage-BiUn8c0z.js.map} +1 -1
  61. package/frontend/dist/assets/{clipboard-wUZFI68F.js → clipboard-7MOQ-4Cg.js} +2 -2
  62. package/frontend/dist/assets/{clipboard-wUZFI68F.js.map → clipboard-7MOQ-4Cg.js.map} +1 -1
  63. package/frontend/dist/assets/index-B3hkLjCv.css +1 -0
  64. package/frontend/dist/assets/index-BKFFbupY.js +3 -0
  65. package/frontend/dist/assets/index-BKFFbupY.js.map +1 -0
  66. package/frontend/dist/assets/{resourceService-D7yitP7i.js → resourceService-BOn21QbP.js} +2 -2
  67. package/frontend/dist/assets/{resourceService-D7yitP7i.js.map → resourceService-BOn21QbP.js.map} +1 -1
  68. package/frontend/dist/assets/{useCostData-33G6s4EW.js → useCostData-3iIVsNH6.js} +2 -2
  69. package/frontend/dist/assets/{useCostData-33G6s4EW.js.map → useCostData-3iIVsNH6.js.map} +1 -1
  70. package/frontend/dist/assets/variableDetection-Cpa0rIrf.js +18 -0
  71. package/frontend/dist/assets/variableDetection-Cpa0rIrf.js.map +1 -0
  72. package/frontend/dist/index.html +2 -2
  73. package/package.json +1 -1
  74. package/frontend/dist/assets/CredentialsPage-CPyZrp4Z.js +0 -2
  75. package/frontend/dist/assets/CredentialsPage-CPyZrp4Z.js.map +0 -1
  76. package/frontend/dist/assets/GroupsPage-VKHjil-L.js +0 -33
  77. package/frontend/dist/assets/GroupsPage-VKHjil-L.js.map +0 -1
  78. package/frontend/dist/assets/ServersPage-DJBurfOe.js.map +0 -1
  79. package/frontend/dist/assets/index--NhF_yyy.css +0 -1
  80. package/frontend/dist/assets/index-BbogfcdA.js +0 -3
  81. package/frontend/dist/assets/index-BbogfcdA.js.map +0 -1
  82. package/frontend/dist/assets/variableDetection-BVSe56ia.js +0 -16
  83. package/frontend/dist/assets/variableDetection-BVSe56ia.js.map +0 -1
@@ -5,6 +5,7 @@ import { preserveListFreshness, combineListFreshness, remainingListTtl, } from '
5
5
  import { expandServerConfig, getServerEnvironment } from './serverConfigEnvironment.js';
6
6
  import { getMcpRequestGroup } from '../utils/mcpRequestGroup.js';
7
7
  import { LegacyMcpClient } from '../clients/legacyMcpClient.js';
8
+ import { getElicitationCapabilities, installElicitationRelay, isElicitationClientClosed, withElicitation, } from './elicitationService.js';
8
9
  import { LEGACY_PROTOCOL_VERSIONS } from '../utils/mcpProtocol.js';
9
10
  import { AsyncLocalStorage } from 'node:async_hooks';
10
11
  import { createHash } from 'node:crypto';
@@ -15,7 +16,7 @@ import { applyHiddenParams, findToolConfig, removeHiddenArguments, } from '../ut
15
16
  import { PrincipalRuntimeService } from './principalRuntimeService.js';
16
17
  import { UserContextService } from './userContextService.js';
17
18
  import { deleteCredentialBindings, missingCredentialError } from './credentialBindingService.js';
18
- import { CredentialBindingError, hasCredentialTemplate, hasPersonalAuthentication, resolveCredentialArguments, validateCredentialTemplate, } from '../utils/credentialTemplate.js';
19
+ import { CredentialBindingError, hasCredentialTemplate, hasPersonalAuthentication, resolveCredentialArguments, hasUrlCredentialSlots, validateCredentialTemplate, } from '../utils/credentialTemplate.js';
19
20
  import os from 'os';
20
21
  import path from 'path';
21
22
  import fs from 'fs';
@@ -355,12 +356,12 @@ const setSessionIsolatedClient = (sessionId, serverName, client, transport) => {
355
356
  * Used when the server has `perSessionClient: true` in its config.
356
357
  * Uses a creation lock to prevent duplicate connections from concurrent calls.
357
358
  */
358
- const getOrCreateIsolatedClient = async (sessionId, serverInfo) => {
359
+ const getOrCreateIsolatedClient = async (sessionId, serverInfo, downstream) => {
359
360
  // Quick check without lock — already exists
360
361
  const sessionClients = sessionIsolatedClients.get(sessionId);
361
362
  if (sessionClients) {
362
363
  const existing = sessionClients.get(serverInfo.name);
363
- if (existing) {
364
+ if (existing && !isElicitationClientClosed(existing.client)) {
364
365
  return existing;
365
366
  }
366
367
  }
@@ -375,7 +376,7 @@ const getOrCreateIsolatedClient = async (sessionId, serverInfo) => {
375
376
  const sessionClientsAfterLock = sessionIsolatedClients.get(sessionId);
376
377
  if (sessionClientsAfterLock) {
377
378
  const existing = sessionClientsAfterLock.get(serverInfo.name);
378
- if (existing) {
379
+ if (existing && !isElicitationClientClosed(existing.client)) {
379
380
  return existing;
380
381
  }
381
382
  }
@@ -388,7 +389,7 @@ const getOrCreateIsolatedClient = async (sessionId, serverInfo) => {
388
389
  }
389
390
  const reservedClients = sessionIsolatedClients.get(sessionId);
390
391
  const transport = await createTransportFromConfig(serverInfo.name, serverConfig);
391
- const client = createUpstreamMcpClient(serverInfo.name, () => serverInfo);
392
+ const client = createUpstreamMcpClient(serverInfo.name, () => serverInfo, downstream);
392
393
  try {
393
394
  await connectClientWithDiagnostics(client, transport, serverInfo.options || {});
394
395
  }
@@ -840,12 +841,13 @@ const updateServerResourcesCache = (serverInfo, resources) => {
840
841
  const logListChangedRefreshError = (listType) => {
841
842
  logger.warn(`Failed to refresh ${listType} list after upstream notification`);
842
843
  };
843
- const createUpstreamMcpClient = (name, getServerInfo) => {
844
- return new LegacyMcpClient({
844
+ const createUpstreamMcpClient = (name, getServerInfo, downstream) => {
845
+ const elicitation = getElicitationCapabilities(downstream);
846
+ const client = new LegacyMcpClient({
845
847
  name: `mcp-client-${name}`,
846
848
  version: '1.0.0',
847
849
  }, {
848
- capabilities: MCP_APPS_CAPABILITIES,
850
+ capabilities: { ...MCP_APPS_CAPABILITIES, ...(elicitation ? { elicitation } : {}) },
849
851
  jsonSchemaValidator: new ResilientJsonSchemaValidator(),
850
852
  listChanged: {
851
853
  tools: {
@@ -895,6 +897,9 @@ const createUpstreamMcpClient = (name, getServerInfo) => {
895
897
  },
896
898
  },
897
899
  });
900
+ if (elicitation && downstream)
901
+ installElicitationRelay(client, downstream);
902
+ return client;
898
903
  };
899
904
  // Normalize and infer server type for safe client display
900
905
  const normalizeServerType = (type) => {
@@ -1229,7 +1234,7 @@ export const createTransportFromConfig = async (name, conf) => {
1229
1234
  // skip the internal-IP blocklist. allowInternal also governs per-hop
1230
1235
  // redirect validation in createRedirectValidatingFetch below.
1231
1236
  const ownerUser = conf.owner ? await getUserDao().findByUsername(conf.owner) : null;
1232
- const allowInternal = !!ownerUser?.isAdmin;
1237
+ const allowInternal = !!ownerUser?.isAdmin && !hasUrlCredentialSlots(conf);
1233
1238
  if (conf.url) {
1234
1239
  await assertSafeUrl(conf.url, { allowInternal });
1235
1240
  }
@@ -1370,7 +1375,7 @@ const withoutHiddenArguments = (serverInfo, toolName, args) => {
1370
1375
  return removeHiddenArguments(args, tool?.inputSchema, toolConfig);
1371
1376
  };
1372
1377
  // Helper function to handle client.callTool with reconnection logic
1373
- const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries = 1, isolated) => {
1378
+ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries = 1, isolated, extra) => {
1374
1379
  // Local, reassignable refs so a reconnect can swap in the new connection for
1375
1380
  // the next retry attempt. For isolated calls these track the per-session
1376
1381
  // client/transport; otherwise they mirror the shared serverInfo connection.
@@ -1389,12 +1394,23 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
1389
1394
  }
1390
1395
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
1391
1396
  try {
1392
- const result = await client.callTool(toolParams, options || {});
1393
- // Check auth error
1394
- 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
+ });
1395
1403
  return result;
1396
1404
  }
1397
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;
1398
1414
  const isHttp40xError = isRecoverableHttp4xxError(error);
1399
1415
  // Only retry for StreamableHTTPClientTransport and SSE transports
1400
1416
  const isStreamableHttp = transport instanceof StreamableHTTPClientTransport;
@@ -2107,7 +2123,7 @@ export const getServersInfo = async (page, limit, user) => {
2107
2123
  }));
2108
2124
  const infos = presentedInfos
2109
2125
  .filter((info) => requestedServerNames.has(info.name)) // Only include requested servers
2110
- .map(({ name, version, packageVersion, latestVersion, updateAvailable, instructions, owner, visibility, status, tools, prompts, resources, createTime, error, oauth, }) => {
2126
+ .map(({ name, version, client, packageVersion, latestVersion, updateAvailable, instructions, owner, visibility, status, tools, prompts, resources, createTime, error, oauth, }) => {
2111
2127
  const serverConfig = allServers.find((server) => server.name === name);
2112
2128
  const enabled = serverConfig ? serverConfig.enabled !== false : true;
2113
2129
  const resolvedType = inferServerType(serverConfig);
@@ -2138,6 +2154,9 @@ export const getServersInfo = async (page, limit, user) => {
2138
2154
  return {
2139
2155
  name,
2140
2156
  version,
2157
+ protocolVersion: enabled && status === 'connected' && resolvedType !== 'openapi'
2158
+ ? client?.getNegotiatedProtocolVersion?.()
2159
+ : undefined,
2141
2160
  instructions,
2142
2161
  owner,
2143
2162
  visibility,
@@ -2940,24 +2959,25 @@ const getMcpAppsRouteContext = async (sessionId, group) => {
2940
2959
  return eligible.length > 0 ? { enabled: true, serverInfos: eligible } : { enabled: false };
2941
2960
  }
2942
2961
  const { filteredServerInfos, serverConfigsByName } = await getFilteredServerInfosForGroup(group);
2943
- const connectedServerInfos = filteredServerInfos.filter((serverInfo) => serverInfo.status === 'connected' && !!serverInfo.client);
2944
- if (connectedServerInfos.length === 0) {
2962
+ const eligibleServerInfos = filteredServerInfos.filter((serverInfo) => canServeToolRequests(serverInfo) &&
2963
+ (!!serverInfo.client || serverInfo.config?.startOnDemand === true));
2964
+ if (eligibleServerInfos.length === 0) {
2945
2965
  return { enabled: false };
2946
2966
  }
2947
2967
  // Connection availability must not turn an aggregate route into a raw-name
2948
2968
  // route: OpenAPI uses openApiClient, and tool calls acquire only the target
2949
2969
  // personal runtime. Match resolveToolInGroup's configured-route ambiguity check.
2950
- if (connectedServerInfos.length === 1 &&
2970
+ if (eligibleServerInfos.length === 1 &&
2951
2971
  filteredServerInfos.length === 1 &&
2952
2972
  serverConfigsByName.size <= 1) {
2953
2973
  return {
2954
2974
  enabled: true,
2955
- serverInfo: connectedServerInfos[0],
2975
+ serverInfo: eligibleServerInfos[0],
2956
2976
  };
2957
2977
  }
2958
2978
  return {
2959
2979
  enabled: true,
2960
- serverInfos: connectedServerInfos,
2980
+ serverInfos: eligibleServerInfos,
2961
2981
  };
2962
2982
  };
2963
2983
  const normalizeToolNameForServer = (serverName, toolName) => {
@@ -2992,9 +3012,14 @@ const projectNameForGroup = (name, serverName, serverConfig) => {
2992
3012
  const resolveNameFromGroup = (name, serverName, serverConfig) => {
2993
3013
  return replacePrefixedServerName(name, getExposedServerName(serverName, serverConfig), serverName);
2994
3014
  };
2995
- const findToolOnServer = (serverInfo, toolName, allowRawName) => {
2996
- const tool = serverInfo.tools.find((tool) => tool.name === toolName ||
2997
- (allowRawName && normalizeToolNameForServer(serverInfo.name, tool.name) === toolName));
3015
+ const findToolOnServer = (serverInfo, toolName, allowRawName, preferRawName = false) => {
3016
+ // A listed upstream name takes precedence over a colliding legacy cache alias.
3017
+ const rawTool = preferRawName && allowRawName
3018
+ ? serverInfo.tools.find((tool) => normalizeToolNameForServer(serverInfo.name, tool.name) === toolName)
3019
+ : undefined;
3020
+ const tool = rawTool ||
3021
+ serverInfo.tools.find((tool) => tool.name === toolName ||
3022
+ (allowRawName && normalizeToolNameForServer(serverInfo.name, tool.name) === toolName));
2998
3023
  if (!tool) {
2999
3024
  return undefined;
3000
3025
  }
@@ -3043,7 +3068,7 @@ const resolveToolInGroup = async (group, toolName) => {
3043
3068
  }
3044
3069
  const serverConfig = serverConfigsByName.get(serverInfo.name);
3045
3070
  const internalToolName = resolveNameFromGroup(toolName, serverInfo.name, serverConfig);
3046
- const tool = findToolOnServer(serverInfo, internalToolName, allowRawName);
3071
+ const tool = findToolOnServer(serverInfo, internalToolName, allowRawName, serverConfigsByName.size === 0);
3047
3072
  if (!tool) {
3048
3073
  continue;
3049
3074
  }
@@ -3055,6 +3080,17 @@ const resolveToolInGroup = async (group, toolName) => {
3055
3080
  }
3056
3081
  return undefined;
3057
3082
  };
3083
+ const findEnabledPromptOnServer = async (serverInfo, name, allowRawName = false) => {
3084
+ if (serverInfo.status !== 'connected' || serverInfo.enabled === false)
3085
+ return undefined;
3086
+ const prompt = serverInfo.prompts.find((item) => item.name === name ||
3087
+ (allowRawName && item.name === `${serverInfo.name}${getNameSeparator()}${name}`));
3088
+ if (!prompt)
3089
+ return undefined;
3090
+ const config = await getServerDao().findById(serverInfo.name);
3091
+ const promptConfig = findToolConfig(config?.prompts, serverInfo.name, prompt.name, serverInfo.prompts.map((item) => item.name));
3092
+ return config?.enabled === false || promptConfig?.enabled === false ? undefined : prompt;
3093
+ };
3058
3094
  async function resolvePromptInGroup(group, promptName) {
3059
3095
  const lookupGroup = getGroupLookupName(group);
3060
3096
  if (!lookupGroup) {
@@ -3067,7 +3103,7 @@ async function resolvePromptInGroup(group, promptName) {
3067
3103
  }
3068
3104
  const serverConfig = serverConfigsByName.get(serverInfo.name);
3069
3105
  const internalPromptName = resolveNameFromGroup(promptName, serverInfo.name, serverConfig);
3070
- const prompt = serverInfo.prompts.find((item) => item.name === internalPromptName);
3106
+ const prompt = await findEnabledPromptOnServer(serverInfo, internalPromptName);
3071
3107
  if (!prompt) {
3072
3108
  continue;
3073
3109
  }
@@ -3079,21 +3115,19 @@ async function resolvePromptInGroup(group, promptName) {
3079
3115
  }
3080
3116
  return undefined;
3081
3117
  }
3082
- const projectToolForDownstream = (serverName, tool, appsRouteContext, serverConfig) => {
3118
+ const projectToolForDownstream = (serverName, tool, appsRouteContext, serverConfig, useRawName = false) => {
3083
3119
  if (!appsRouteContext.enabled && isAppOnlyTool(tool)) {
3084
3120
  return undefined;
3085
3121
  }
3086
3122
  const projectedTool = appsRouteContext.enabled ? tool : stripMcpAppsMetadata(tool);
3087
- // Single configured server: keep the existing raw (unqualified) name so
3088
- // widgets that call back with the bare tool name continue to work.
3089
- if (appsRouteContext.serverInfo) {
3123
+ // Naming follows the route, independently of Apps capabilities or connection state.
3124
+ if (useRawName) {
3090
3125
  return {
3091
3126
  ...projectedTool,
3092
3127
  name: normalizeToolNameForServer(serverName, projectedTool.name),
3093
3128
  };
3094
3129
  }
3095
- // Aggregate routes (Apps-enabled) or Apps disabled: qualify the
3096
- // name so it can still be routed to the right server unambiguously.
3130
+ // Group and aggregate routes keep qualified names, including single-member groups.
3097
3131
  return {
3098
3132
  ...projectedTool,
3099
3133
  name: projectNameForGroup(projectedTool.name, serverName, serverConfig),
@@ -3106,6 +3140,7 @@ const listGroupTools = async (group, filteredServerInfos, serverConfigsByName, a
3106
3140
  await Promise.allSettled(filteredServerInfos
3107
3141
  .filter((si) => si.config?.startOnDemand === true && si.tools.length === 0 && si.spawningPromise)
3108
3142
  .map((si) => si.spawningPromise));
3143
+ const useRawName = !!group && !isSmartRoutingGroup(group) && !(await findRouteGroup(group));
3109
3144
  const allTools = [];
3110
3145
  for (const serverInfo of filteredServerInfos) {
3111
3146
  if (serverInfo.tools && serverInfo.tools.length > 0) {
@@ -3125,7 +3160,7 @@ const listGroupTools = async (group, filteredServerInfos, serverConfigsByName, a
3125
3160
  };
3126
3161
  });
3127
3162
  allTools.push(...toolsWithCustomDescriptions.flatMap((tool) => {
3128
- const projectedTool = projectToolForDownstream(serverInfo.name, tool, appsRouteContext, groupServerConfig);
3163
+ const projectedTool = projectToolForDownstream(serverInfo.name, tool, appsRouteContext, groupServerConfig, useRawName);
3129
3164
  return projectedTool ? [projectedTool] : [];
3130
3165
  }));
3131
3166
  }
@@ -3282,28 +3317,33 @@ const handleCallToolRequestImpl = async (request, extra) => {
3282
3317
  };
3283
3318
  try {
3284
3319
  appsRouteContext = await getMcpAppsRouteContext(sessionId, group);
3320
+ const lookupRoute = getGroupLookupName(group);
3321
+ // Direct routes own their listed raw names, even when a tool is disabled.
3322
+ const upstreamOwnsMetaName = SMART_ROUTING_META_TOOL_NAMES.has(request.params.name) &&
3323
+ !!lookupRoute &&
3324
+ !isSmartRoutingGroup(group) &&
3325
+ !(await findRouteGroup(lookupRoute)) &&
3326
+ getVisibleServerByName(lookupRoute)?.tools.some((tool) => normalizeToolNameForServer(lookupRoute, tool.name) === request.params.name);
3285
3327
  // Special handling for smart routing tools
3286
- if (request.params.name === 'search_tools') {
3328
+ if (!upstreamOwnsMetaName && request.params.name === 'search_tools') {
3287
3329
  const { query, limit = 10 } = request.params.arguments || {};
3288
3330
  return await handleSearchToolsRequest(query, limit, sessionId, group);
3289
3331
  }
3290
3332
  // Special handling for describe_tool (progressive disclosure mode)
3291
- if (request.params.name === 'describe_tool') {
3333
+ if (!upstreamOwnsMetaName && request.params.name === 'describe_tool') {
3292
3334
  const { toolName } = request.params.arguments || {};
3293
3335
  return await handleDescribeToolRequest(toolName, sessionId, group);
3294
3336
  }
3295
3337
  // Special handling for call_tool
3296
- if (request.params.name === 'call_tool') {
3338
+ if (!upstreamOwnsMetaName && request.params.name === 'call_tool') {
3297
3339
  const { toolName } = request.params.arguments || {};
3298
3340
  if (!toolName) {
3299
3341
  throw new Error('toolName parameter is required');
3300
3342
  }
3301
3343
  const { arguments: toolArgs } = request.params.arguments || {};
3302
- // A single connected server on an Apps-enabled route is addressed by
3303
- // its raw (unqualified) tool name, same as before. A multi-server
3304
- // Apps-enabled route falls through to qualified-name group
3305
- // resolution below, same as an ordinary (non-Apps) group route.
3306
- const singleServerAppsRoute = !!appsRouteContext.serverInfo;
3344
+ // Preserve legacy raw aggregate calls; explicit routes use the shared
3345
+ // resolver so cached names still obey group filtering.
3346
+ const singleServerAppsRoute = !getGroupLookupName(group) && !!appsRouteContext.serverInfo;
3307
3347
  let targetServerInfo;
3308
3348
  let targetToolName = toolName;
3309
3349
  let targetTool;
@@ -3364,7 +3404,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
3364
3404
  arguments: summarizeArgumentsForLogging(finalArgs),
3365
3405
  });
3366
3406
  // Remove server prefix from tool name if present
3367
- const cleanToolName = normalizeToolNameForServer(targetServerInfo.name, targetToolName);
3407
+ const cleanToolName = normalizeToolNameForServer(targetServerInfo.name, tool.name);
3368
3408
  // Extract passthrough headers from extra or request context
3369
3409
  let passthroughHeaders;
3370
3410
  let requestHeaders = null;
@@ -3447,7 +3487,9 @@ const handleCallToolRequestImpl = async (request, extra) => {
3447
3487
  if (!stateId) {
3448
3488
  throw new Error(`Server '${targetServerInfo.name}' requires per-session client isolation and is not yet available on stateless MCP requests`);
3449
3489
  }
3450
- const isolated = await getOrCreateIsolatedClient(stateId, targetServerInfo);
3490
+ const isolated = await getOrCreateIsolatedClient(stateId, targetServerInfo, extra.mcpReq && !requestContextService.getRequestContext()?.stateless
3491
+ ? servers[sessionId]
3492
+ : undefined);
3451
3493
  isolatedCtx = {
3452
3494
  sessionId: stateId,
3453
3495
  client: isolated.client,
@@ -3465,12 +3507,12 @@ const handleCallToolRequestImpl = async (request, extra) => {
3465
3507
  arguments: summarizeArgumentsForLogging(finalArgs),
3466
3508
  perSessionClient: !!targetServerInfo.config?.perSessionClient,
3467
3509
  });
3468
- const cleanToolName = normalizeToolNameForServer(targetServerInfo.name, targetToolName);
3510
+ const cleanToolName = normalizeToolNameForServer(targetServerInfo.name, tool.name);
3469
3511
  await reserveHostedIfNeeded(targetServerInfo.name, cleanToolName);
3470
3512
  const result = await callToolWithReconnect(targetServerInfo, {
3471
3513
  name: cleanToolName,
3472
3514
  arguments: finalArgs,
3473
- }, targetServerInfo.options || {}, 1, isolatedCtx);
3515
+ }, targetServerInfo.options || {}, 1, isolatedCtx, extra);
3474
3516
  await settleHostedIfNeeded({
3475
3517
  success: !result.isError,
3476
3518
  requestContent: finalArgs,
@@ -3506,13 +3548,9 @@ const handleCallToolRequestImpl = async (request, extra) => {
3506
3548
  }
3507
3549
  // Regular tool handling
3508
3550
  const lookupGroup = getGroupLookupName(group);
3509
- // A single connected server on an Apps-enabled route is addressed
3510
- // directly, using its raw (unqualified) tool name, same as before. A
3511
- // multi-server Apps-enabled route resolves via the qualified-name
3512
- // group lookup below, same as an ordinary (non-Apps) group route,
3513
- // just without stripping Apps metadata or hiding app-only tools
3514
- // (assertToolAvailableForRoute below still gates on appsRouteContext.enabled).
3515
- const singleServerAppsRoute = !!appsRouteContext.serverInfo;
3551
+ // Explicit routes share name resolution and group filtering regardless of
3552
+ // Apps support; keep legacy raw aggregate calls compatible.
3553
+ const singleServerAppsRoute = !getGroupLookupName(group) && !!appsRouteContext.serverInfo;
3516
3554
  const groupTool = !singleServerAppsRoute && lookupGroup
3517
3555
  ? await resolveToolInGroup(group, request.params.name)
3518
3556
  : undefined;
@@ -3540,7 +3578,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
3540
3578
  // For OpenAPI servers, use the OpenAPI client
3541
3579
  const openApiClient = serverInfo.openApiClient;
3542
3580
  // Remove server prefix from tool name if present
3543
- const cleanToolName = normalizeToolNameForServer(serverInfo.name, routeToolName);
3581
+ const cleanToolName = normalizeToolNameForServer(serverInfo.name, tool.name);
3544
3582
  logger.log('Invoking OpenAPI tool', {
3545
3583
  toolName: cleanToolName,
3546
3584
  serverName: serverInfo.name,
@@ -3628,15 +3666,17 @@ const handleCallToolRequestImpl = async (request, extra) => {
3628
3666
  if (!stateId) {
3629
3667
  throw new Error(`Server '${serverInfo.name}' requires per-session client isolation and is not yet available on stateless MCP requests`);
3630
3668
  }
3631
- const isolated = await getOrCreateIsolatedClient(stateId, serverInfo);
3669
+ const isolated = await getOrCreateIsolatedClient(stateId, serverInfo, extra.mcpReq && !requestContextService.getRequestContext()?.stateless
3670
+ ? servers[sessionId]
3671
+ : undefined);
3632
3672
  isolatedCtx = { sessionId: stateId, client: isolated.client, transport: isolated.transport };
3633
3673
  }
3634
3674
  else if (!serverInfo.client) {
3635
3675
  throw new Error(`Client not found for server: ${serverInfo.name}`);
3636
3676
  }
3637
- const cleanToolName = normalizeToolNameForServer(serverInfo.name, routeToolName);
3677
+ const cleanToolName = normalizeToolNameForServer(serverInfo.name, tool.name);
3638
3678
  await reserveHostedIfNeeded(serverInfo.name, cleanToolName);
3639
- const result = await callToolWithReconnect(serverInfo, { ...request.params, name: cleanToolName }, serverInfo.options || {}, 1, isolatedCtx);
3679
+ const result = await callToolWithReconnect(serverInfo, { ...request.params, name: cleanToolName }, serverInfo.options || {}, 1, isolatedCtx, extra);
3640
3680
  await settleHostedIfNeeded({
3641
3681
  success: !result.isError,
3642
3682
  requestContent: request.params.arguments,
@@ -3735,7 +3775,7 @@ const handleGetPromptRequestImpl = async (request, extra) => {
3735
3775
  const { name, arguments: promptArgs } = request.params;
3736
3776
  const group = getMcpRequestGroup(extra);
3737
3777
  // Check built-in prompts first
3738
- const builtinPrompt = await getBuiltinPromptDao().findByName(name);
3778
+ const builtinPrompt = !extra?.server && (await getBuiltinPromptDao().findByName(name));
3739
3779
  if (builtinPrompt && builtinPrompt.enabled !== false) {
3740
3780
  // Perform {{param}} template substitution
3741
3781
  let content = builtinPrompt.template;
@@ -3757,7 +3797,12 @@ const handleGetPromptRequestImpl = async (request, extra) => {
3757
3797
  let promptNameForServer = name;
3758
3798
  const lookupGroup = getGroupLookupName(group);
3759
3799
  if (extra && extra.server) {
3760
- server = getVisibleServerByName(extra.server);
3800
+ const candidate = getVisibleServerByName(extra.server);
3801
+ const prompt = candidate && (await findEnabledPromptOnServer(candidate, name, true));
3802
+ if (prompt) {
3803
+ server = candidate;
3804
+ promptNameForServer = prompt.name;
3805
+ }
3761
3806
  }
3762
3807
  else if (lookupGroup) {
3763
3808
  const groupPrompt = await resolvePromptInGroup(lookupGroup, name);
@@ -3767,10 +3812,14 @@ const handleGetPromptRequestImpl = async (request, extra) => {
3767
3812
  }
3768
3813
  }
3769
3814
  else {
3770
- // Find the first server that has this prompt
3771
- server = getVisibleServerInfos().find((serverInfo) => serverInfo.status === 'connected' &&
3772
- serverInfo.enabled !== false &&
3773
- serverInfo.prompts.find((prompt) => prompt.name === name));
3815
+ for (const candidate of getVisibleServerInfos()) {
3816
+ const prompt = await findEnabledPromptOnServer(candidate, name);
3817
+ if (prompt) {
3818
+ server = candidate;
3819
+ promptNameForServer = prompt.name;
3820
+ break;
3821
+ }
3822
+ }
3774
3823
  }
3775
3824
  if (!server) {
3776
3825
  throw new ToolUnavailableError(`Prompt not available: ${name}`, classifyUnavailableReason(name));
@@ -3839,7 +3888,7 @@ const handleListPromptsRequestImpl = async (_, extra) => {
3839
3888
  let enabledPrompts = serverInfo.prompts;
3840
3889
  if (serverConfig && serverConfig.prompts) {
3841
3890
  enabledPrompts = serverInfo.prompts.filter((prompt) => {
3842
- const promptConfig = serverConfig.prompts?.[prompt.name];
3891
+ const promptConfig = findToolConfig(serverConfig.prompts, serverInfo.name, prompt.name, serverInfo.prompts.map((item) => item.name));
3843
3892
  // If prompt is not in config, it's enabled by default
3844
3893
  return promptConfig?.enabled !== false;
3845
3894
  });
@@ -4006,6 +4055,12 @@ const handleReadResourceRequestImpl = async (request, extra) => {
4006
4055
  ? [appsRouteContext.serverInfo]
4007
4056
  : (appsRouteContext.serverInfos ?? []);
4008
4057
  for (const candidate of candidates) {
4058
+ const config = await getServerDao().findById(candidate.name);
4059
+ if (config?.resources?.[uri]?.enabled === false)
4060
+ continue;
4061
+ const allowed = await filterResourcesByGroup(lookupGroup, candidate.name, [{ uri }], serverConfigsByName.get(candidate.name));
4062
+ if (allowed.length === 0)
4063
+ continue;
4009
4064
  // An on-demand candidate may be idle yet still advertise its cached widget
4010
4065
  // link, so a host can read the ui:// resource before any tool call has
4011
4066
  // woken it. Hold off its idle shutdown for every read, awake or not, so an
@@ -4201,7 +4256,7 @@ const withPrincipalServers = (handler, operation) => async (request, extra) => {
4201
4256
  if (!serverInfos.some((info) => hasPersonalAuthentication(info.config)))
4202
4257
  return handler(request, extra);
4203
4258
  // Built-in content does not require an upstream credential.
4204
- if (operation === 'prompt') {
4259
+ if (operation === 'prompt' && !extra?.server) {
4205
4260
  const builtin = await getBuiltinPromptDao().findByName(request.params?.name);
4206
4261
  if (builtin && builtin.enabled !== false)
4207
4262
  return handler(request, extra);