@samanhappy/mcphub 1.1.8 → 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 (82) 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 +113 -61
  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-CFRX20v4.js} +2 -2
  28. package/frontend/dist/assets/{ActivityPage-CJdAfOUF.js.map → ActivityPage-CFRX20v4.js.map} +1 -1
  29. package/frontend/dist/assets/{CopyClientConfigDialog-DKktm8jj.js → CopyClientConfigDialog-CDEiSInj.js} +2 -2
  30. package/frontend/dist/assets/{CopyClientConfigDialog-DKktm8jj.js.map → CopyClientConfigDialog-CDEiSInj.js.map} +1 -1
  31. package/frontend/dist/assets/CredentialsPage-BCLHY_fN.js +2 -0
  32. package/frontend/dist/assets/CredentialsPage-BCLHY_fN.js.map +1 -0
  33. package/frontend/dist/assets/{Dashboard-DM_IiBIT.js → Dashboard-B3Y6nAFL.js} +2 -2
  34. package/frontend/dist/assets/{Dashboard-DM_IiBIT.js.map → Dashboard-B3Y6nAFL.js.map} +1 -1
  35. package/frontend/dist/assets/{EndpointCopy-DHQjJesJ.js → EndpointCopy-BMxT99Ne.js} +2 -2
  36. package/frontend/dist/assets/{EndpointCopy-DHQjJesJ.js.map → EndpointCopy-BMxT99Ne.js.map} +1 -1
  37. package/frontend/dist/assets/GroupsPage-5iCk1LKT.js +33 -0
  38. package/frontend/dist/assets/GroupsPage-5iCk1LKT.js.map +1 -0
  39. package/frontend/dist/assets/{LoginPage-CKrI0By4.js → LoginPage-BjYzY6RH.js} +2 -2
  40. package/frontend/dist/assets/{LoginPage-CKrI0By4.js.map → LoginPage-BjYzY6RH.js.map} +1 -1
  41. package/frontend/dist/assets/{LogsPage-Dh8gRpEH.js → LogsPage-CGcFNKJn.js} +2 -2
  42. package/frontend/dist/assets/{LogsPage-Dh8gRpEH.js.map → LogsPage-CGcFNKJn.js.map} +1 -1
  43. package/frontend/dist/assets/{MarketPage-BN4qL8L9.js → MarketPage-B7JPqFw3.js} +2 -2
  44. package/frontend/dist/assets/{MarketPage-BN4qL8L9.js.map → MarketPage-B7JPqFw3.js.map} +1 -1
  45. package/frontend/dist/assets/{OAuthConsentPage-Cyj-rrA4.js → OAuthConsentPage-BYi_kgNj.js} +2 -2
  46. package/frontend/dist/assets/{OAuthConsentPage-Cyj-rrA4.js.map → OAuthConsentPage-BYi_kgNj.js.map} +1 -1
  47. package/frontend/dist/assets/{PromptsPage-YWD04XeI.js → PromptsPage-x6X5trLX.js} +2 -2
  48. package/frontend/dist/assets/{PromptsPage-YWD04XeI.js.map → PromptsPage-x6X5trLX.js.map} +1 -1
  49. package/frontend/dist/assets/{ResourcesPage-CVRzHCf4.js → ResourcesPage-BJZCPBs8.js} +2 -2
  50. package/frontend/dist/assets/{ResourcesPage-CVRzHCf4.js.map → ResourcesPage-BJZCPBs8.js.map} +1 -1
  51. package/frontend/dist/assets/{ServersPage-DJBurfOe.js → ServersPage-CsyGLden.js} +4 -4
  52. package/frontend/dist/assets/{ServersPage-DJBurfOe.js.map → ServersPage-CsyGLden.js.map} +1 -1
  53. package/frontend/dist/assets/{SettingsPage-3sK6NJRn.js → SettingsPage-C53DI0Sz.js} +2 -2
  54. package/frontend/dist/assets/{SettingsPage-3sK6NJRn.js.map → SettingsPage-C53DI0Sz.js.map} +1 -1
  55. package/frontend/dist/assets/{StatusDot-CLZidCyq.js → StatusDot-BDJd3Iix.js} +2 -2
  56. package/frontend/dist/assets/{StatusDot-CLZidCyq.js.map → StatusDot-BDJd3Iix.js.map} +1 -1
  57. package/frontend/dist/assets/{ToggleGroup-Dfh4jKpP.js → ToggleGroup-BbmRhILg.js} +2 -2
  58. package/frontend/dist/assets/{ToggleGroup-Dfh4jKpP.js.map → ToggleGroup-BbmRhILg.js.map} +1 -1
  59. package/frontend/dist/assets/{UsersPage-DfFqSRVb.js → UsersPage-NUG6vF8C.js} +2 -2
  60. package/frontend/dist/assets/{UsersPage-DfFqSRVb.js.map → UsersPage-NUG6vF8C.js.map} +1 -1
  61. package/frontend/dist/assets/{clipboard-wUZFI68F.js → clipboard-7BggsilD.js} +2 -2
  62. package/frontend/dist/assets/{clipboard-wUZFI68F.js.map → clipboard-7BggsilD.js.map} +1 -1
  63. package/frontend/dist/assets/index-B_IITM9e.js +3 -0
  64. package/frontend/dist/assets/index-B_IITM9e.js.map +1 -0
  65. package/frontend/dist/assets/index-CnqLKewL.css +1 -0
  66. package/frontend/dist/assets/{resourceService-D7yitP7i.js → resourceService-B674Fopr.js} +2 -2
  67. package/frontend/dist/assets/{resourceService-D7yitP7i.js.map → resourceService-B674Fopr.js.map} +1 -1
  68. package/frontend/dist/assets/{useCostData-33G6s4EW.js → useCostData-8Z1HjAL5.js} +2 -2
  69. package/frontend/dist/assets/{useCostData-33G6s4EW.js.map → useCostData-8Z1HjAL5.js.map} +1 -1
  70. package/frontend/dist/assets/variableDetection-C0nP3Jj2.js +18 -0
  71. package/frontend/dist/assets/variableDetection-C0nP3Jj2.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/index--NhF_yyy.css +0 -1
  79. package/frontend/dist/assets/index-BbogfcdA.js +0 -3
  80. package/frontend/dist/assets/index-BbogfcdA.js.map +0 -1
  81. package/frontend/dist/assets/variableDetection-BVSe56ia.js +0 -16
  82. 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;
@@ -2940,24 +2956,25 @@ const getMcpAppsRouteContext = async (sessionId, group) => {
2940
2956
  return eligible.length > 0 ? { enabled: true, serverInfos: eligible } : { enabled: false };
2941
2957
  }
2942
2958
  const { filteredServerInfos, serverConfigsByName } = await getFilteredServerInfosForGroup(group);
2943
- const connectedServerInfos = filteredServerInfos.filter((serverInfo) => serverInfo.status === 'connected' && !!serverInfo.client);
2944
- if (connectedServerInfos.length === 0) {
2959
+ const eligibleServerInfos = filteredServerInfos.filter((serverInfo) => canServeToolRequests(serverInfo) &&
2960
+ (!!serverInfo.client || serverInfo.config?.startOnDemand === true));
2961
+ if (eligibleServerInfos.length === 0) {
2945
2962
  return { enabled: false };
2946
2963
  }
2947
2964
  // Connection availability must not turn an aggregate route into a raw-name
2948
2965
  // route: OpenAPI uses openApiClient, and tool calls acquire only the target
2949
2966
  // personal runtime. Match resolveToolInGroup's configured-route ambiguity check.
2950
- if (connectedServerInfos.length === 1 &&
2967
+ if (eligibleServerInfos.length === 1 &&
2951
2968
  filteredServerInfos.length === 1 &&
2952
2969
  serverConfigsByName.size <= 1) {
2953
2970
  return {
2954
2971
  enabled: true,
2955
- serverInfo: connectedServerInfos[0],
2972
+ serverInfo: eligibleServerInfos[0],
2956
2973
  };
2957
2974
  }
2958
2975
  return {
2959
2976
  enabled: true,
2960
- serverInfos: connectedServerInfos,
2977
+ serverInfos: eligibleServerInfos,
2961
2978
  };
2962
2979
  };
2963
2980
  const normalizeToolNameForServer = (serverName, toolName) => {
@@ -2992,9 +3009,14 @@ const projectNameForGroup = (name, serverName, serverConfig) => {
2992
3009
  const resolveNameFromGroup = (name, serverName, serverConfig) => {
2993
3010
  return replacePrefixedServerName(name, getExposedServerName(serverName, serverConfig), serverName);
2994
3011
  };
2995
- const findToolOnServer = (serverInfo, toolName, allowRawName) => {
2996
- const tool = serverInfo.tools.find((tool) => tool.name === toolName ||
2997
- (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));
2998
3020
  if (!tool) {
2999
3021
  return undefined;
3000
3022
  }
@@ -3043,7 +3065,7 @@ const resolveToolInGroup = async (group, toolName) => {
3043
3065
  }
3044
3066
  const serverConfig = serverConfigsByName.get(serverInfo.name);
3045
3067
  const internalToolName = resolveNameFromGroup(toolName, serverInfo.name, serverConfig);
3046
- const tool = findToolOnServer(serverInfo, internalToolName, allowRawName);
3068
+ const tool = findToolOnServer(serverInfo, internalToolName, allowRawName, serverConfigsByName.size === 0);
3047
3069
  if (!tool) {
3048
3070
  continue;
3049
3071
  }
@@ -3055,6 +3077,17 @@ const resolveToolInGroup = async (group, toolName) => {
3055
3077
  }
3056
3078
  return undefined;
3057
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
+ };
3058
3091
  async function resolvePromptInGroup(group, promptName) {
3059
3092
  const lookupGroup = getGroupLookupName(group);
3060
3093
  if (!lookupGroup) {
@@ -3067,7 +3100,7 @@ async function resolvePromptInGroup(group, promptName) {
3067
3100
  }
3068
3101
  const serverConfig = serverConfigsByName.get(serverInfo.name);
3069
3102
  const internalPromptName = resolveNameFromGroup(promptName, serverInfo.name, serverConfig);
3070
- const prompt = serverInfo.prompts.find((item) => item.name === internalPromptName);
3103
+ const prompt = await findEnabledPromptOnServer(serverInfo, internalPromptName);
3071
3104
  if (!prompt) {
3072
3105
  continue;
3073
3106
  }
@@ -3079,21 +3112,19 @@ async function resolvePromptInGroup(group, promptName) {
3079
3112
  }
3080
3113
  return undefined;
3081
3114
  }
3082
- const projectToolForDownstream = (serverName, tool, appsRouteContext, serverConfig) => {
3115
+ const projectToolForDownstream = (serverName, tool, appsRouteContext, serverConfig, useRawName = false) => {
3083
3116
  if (!appsRouteContext.enabled && isAppOnlyTool(tool)) {
3084
3117
  return undefined;
3085
3118
  }
3086
3119
  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) {
3120
+ // Naming follows the route, independently of Apps capabilities or connection state.
3121
+ if (useRawName) {
3090
3122
  return {
3091
3123
  ...projectedTool,
3092
3124
  name: normalizeToolNameForServer(serverName, projectedTool.name),
3093
3125
  };
3094
3126
  }
3095
- // Aggregate routes (Apps-enabled) or Apps disabled: qualify the
3096
- // name so it can still be routed to the right server unambiguously.
3127
+ // Group and aggregate routes keep qualified names, including single-member groups.
3097
3128
  return {
3098
3129
  ...projectedTool,
3099
3130
  name: projectNameForGroup(projectedTool.name, serverName, serverConfig),
@@ -3106,6 +3137,7 @@ const listGroupTools = async (group, filteredServerInfos, serverConfigsByName, a
3106
3137
  await Promise.allSettled(filteredServerInfos
3107
3138
  .filter((si) => si.config?.startOnDemand === true && si.tools.length === 0 && si.spawningPromise)
3108
3139
  .map((si) => si.spawningPromise));
3140
+ const useRawName = !!group && !isSmartRoutingGroup(group) && !(await findRouteGroup(group));
3109
3141
  const allTools = [];
3110
3142
  for (const serverInfo of filteredServerInfos) {
3111
3143
  if (serverInfo.tools && serverInfo.tools.length > 0) {
@@ -3125,7 +3157,7 @@ const listGroupTools = async (group, filteredServerInfos, serverConfigsByName, a
3125
3157
  };
3126
3158
  });
3127
3159
  allTools.push(...toolsWithCustomDescriptions.flatMap((tool) => {
3128
- const projectedTool = projectToolForDownstream(serverInfo.name, tool, appsRouteContext, groupServerConfig);
3160
+ const projectedTool = projectToolForDownstream(serverInfo.name, tool, appsRouteContext, groupServerConfig, useRawName);
3129
3161
  return projectedTool ? [projectedTool] : [];
3130
3162
  }));
3131
3163
  }
@@ -3282,28 +3314,33 @@ const handleCallToolRequestImpl = async (request, extra) => {
3282
3314
  };
3283
3315
  try {
3284
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);
3285
3324
  // Special handling for smart routing tools
3286
- if (request.params.name === 'search_tools') {
3325
+ if (!upstreamOwnsMetaName && request.params.name === 'search_tools') {
3287
3326
  const { query, limit = 10 } = request.params.arguments || {};
3288
3327
  return await handleSearchToolsRequest(query, limit, sessionId, group);
3289
3328
  }
3290
3329
  // Special handling for describe_tool (progressive disclosure mode)
3291
- if (request.params.name === 'describe_tool') {
3330
+ if (!upstreamOwnsMetaName && request.params.name === 'describe_tool') {
3292
3331
  const { toolName } = request.params.arguments || {};
3293
3332
  return await handleDescribeToolRequest(toolName, sessionId, group);
3294
3333
  }
3295
3334
  // Special handling for call_tool
3296
- if (request.params.name === 'call_tool') {
3335
+ if (!upstreamOwnsMetaName && request.params.name === 'call_tool') {
3297
3336
  const { toolName } = request.params.arguments || {};
3298
3337
  if (!toolName) {
3299
3338
  throw new Error('toolName parameter is required');
3300
3339
  }
3301
3340
  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;
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;
3307
3344
  let targetServerInfo;
3308
3345
  let targetToolName = toolName;
3309
3346
  let targetTool;
@@ -3364,7 +3401,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
3364
3401
  arguments: summarizeArgumentsForLogging(finalArgs),
3365
3402
  });
3366
3403
  // Remove server prefix from tool name if present
3367
- const cleanToolName = normalizeToolNameForServer(targetServerInfo.name, targetToolName);
3404
+ const cleanToolName = normalizeToolNameForServer(targetServerInfo.name, tool.name);
3368
3405
  // Extract passthrough headers from extra or request context
3369
3406
  let passthroughHeaders;
3370
3407
  let requestHeaders = null;
@@ -3447,7 +3484,9 @@ const handleCallToolRequestImpl = async (request, extra) => {
3447
3484
  if (!stateId) {
3448
3485
  throw new Error(`Server '${targetServerInfo.name}' requires per-session client isolation and is not yet available on stateless MCP requests`);
3449
3486
  }
3450
- const isolated = await getOrCreateIsolatedClient(stateId, targetServerInfo);
3487
+ const isolated = await getOrCreateIsolatedClient(stateId, targetServerInfo, extra.mcpReq && !requestContextService.getRequestContext()?.stateless
3488
+ ? servers[sessionId]
3489
+ : undefined);
3451
3490
  isolatedCtx = {
3452
3491
  sessionId: stateId,
3453
3492
  client: isolated.client,
@@ -3465,12 +3504,12 @@ const handleCallToolRequestImpl = async (request, extra) => {
3465
3504
  arguments: summarizeArgumentsForLogging(finalArgs),
3466
3505
  perSessionClient: !!targetServerInfo.config?.perSessionClient,
3467
3506
  });
3468
- const cleanToolName = normalizeToolNameForServer(targetServerInfo.name, targetToolName);
3507
+ const cleanToolName = normalizeToolNameForServer(targetServerInfo.name, tool.name);
3469
3508
  await reserveHostedIfNeeded(targetServerInfo.name, cleanToolName);
3470
3509
  const result = await callToolWithReconnect(targetServerInfo, {
3471
3510
  name: cleanToolName,
3472
3511
  arguments: finalArgs,
3473
- }, targetServerInfo.options || {}, 1, isolatedCtx);
3512
+ }, targetServerInfo.options || {}, 1, isolatedCtx, extra);
3474
3513
  await settleHostedIfNeeded({
3475
3514
  success: !result.isError,
3476
3515
  requestContent: finalArgs,
@@ -3506,13 +3545,9 @@ const handleCallToolRequestImpl = async (request, extra) => {
3506
3545
  }
3507
3546
  // Regular tool handling
3508
3547
  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;
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;
3516
3551
  const groupTool = !singleServerAppsRoute && lookupGroup
3517
3552
  ? await resolveToolInGroup(group, request.params.name)
3518
3553
  : undefined;
@@ -3540,7 +3575,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
3540
3575
  // For OpenAPI servers, use the OpenAPI client
3541
3576
  const openApiClient = serverInfo.openApiClient;
3542
3577
  // Remove server prefix from tool name if present
3543
- const cleanToolName = normalizeToolNameForServer(serverInfo.name, routeToolName);
3578
+ const cleanToolName = normalizeToolNameForServer(serverInfo.name, tool.name);
3544
3579
  logger.log('Invoking OpenAPI tool', {
3545
3580
  toolName: cleanToolName,
3546
3581
  serverName: serverInfo.name,
@@ -3628,15 +3663,17 @@ const handleCallToolRequestImpl = async (request, extra) => {
3628
3663
  if (!stateId) {
3629
3664
  throw new Error(`Server '${serverInfo.name}' requires per-session client isolation and is not yet available on stateless MCP requests`);
3630
3665
  }
3631
- const isolated = await getOrCreateIsolatedClient(stateId, serverInfo);
3666
+ const isolated = await getOrCreateIsolatedClient(stateId, serverInfo, extra.mcpReq && !requestContextService.getRequestContext()?.stateless
3667
+ ? servers[sessionId]
3668
+ : undefined);
3632
3669
  isolatedCtx = { sessionId: stateId, client: isolated.client, transport: isolated.transport };
3633
3670
  }
3634
3671
  else if (!serverInfo.client) {
3635
3672
  throw new Error(`Client not found for server: ${serverInfo.name}`);
3636
3673
  }
3637
- const cleanToolName = normalizeToolNameForServer(serverInfo.name, routeToolName);
3674
+ const cleanToolName = normalizeToolNameForServer(serverInfo.name, tool.name);
3638
3675
  await reserveHostedIfNeeded(serverInfo.name, cleanToolName);
3639
- 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);
3640
3677
  await settleHostedIfNeeded({
3641
3678
  success: !result.isError,
3642
3679
  requestContent: request.params.arguments,
@@ -3735,7 +3772,7 @@ const handleGetPromptRequestImpl = async (request, extra) => {
3735
3772
  const { name, arguments: promptArgs } = request.params;
3736
3773
  const group = getMcpRequestGroup(extra);
3737
3774
  // Check built-in prompts first
3738
- const builtinPrompt = await getBuiltinPromptDao().findByName(name);
3775
+ const builtinPrompt = !extra?.server && (await getBuiltinPromptDao().findByName(name));
3739
3776
  if (builtinPrompt && builtinPrompt.enabled !== false) {
3740
3777
  // Perform {{param}} template substitution
3741
3778
  let content = builtinPrompt.template;
@@ -3757,7 +3794,12 @@ const handleGetPromptRequestImpl = async (request, extra) => {
3757
3794
  let promptNameForServer = name;
3758
3795
  const lookupGroup = getGroupLookupName(group);
3759
3796
  if (extra && extra.server) {
3760
- 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
+ }
3761
3803
  }
3762
3804
  else if (lookupGroup) {
3763
3805
  const groupPrompt = await resolvePromptInGroup(lookupGroup, name);
@@ -3767,10 +3809,14 @@ const handleGetPromptRequestImpl = async (request, extra) => {
3767
3809
  }
3768
3810
  }
3769
3811
  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));
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
+ }
3774
3820
  }
3775
3821
  if (!server) {
3776
3822
  throw new ToolUnavailableError(`Prompt not available: ${name}`, classifyUnavailableReason(name));
@@ -3839,7 +3885,7 @@ const handleListPromptsRequestImpl = async (_, extra) => {
3839
3885
  let enabledPrompts = serverInfo.prompts;
3840
3886
  if (serverConfig && serverConfig.prompts) {
3841
3887
  enabledPrompts = serverInfo.prompts.filter((prompt) => {
3842
- const promptConfig = serverConfig.prompts?.[prompt.name];
3888
+ const promptConfig = findToolConfig(serverConfig.prompts, serverInfo.name, prompt.name, serverInfo.prompts.map((item) => item.name));
3843
3889
  // If prompt is not in config, it's enabled by default
3844
3890
  return promptConfig?.enabled !== false;
3845
3891
  });
@@ -4006,6 +4052,12 @@ const handleReadResourceRequestImpl = async (request, extra) => {
4006
4052
  ? [appsRouteContext.serverInfo]
4007
4053
  : (appsRouteContext.serverInfos ?? []);
4008
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;
4009
4061
  // An on-demand candidate may be idle yet still advertise its cached widget
4010
4062
  // link, so a host can read the ui:// resource before any tool call has
4011
4063
  // woken it. Hold off its idle shutdown for every read, awake or not, so an
@@ -4201,7 +4253,7 @@ const withPrincipalServers = (handler, operation) => async (request, extra) => {
4201
4253
  if (!serverInfos.some((info) => hasPersonalAuthentication(info.config)))
4202
4254
  return handler(request, extra);
4203
4255
  // Built-in content does not require an upstream credential.
4204
- if (operation === 'prompt') {
4256
+ if (operation === 'prompt' && !extra?.server) {
4205
4257
  const builtin = await getBuiltinPromptDao().findByName(request.params?.name);
4206
4258
  if (builtin && builtin.enabled !== false)
4207
4259
  return handler(request, extra);