@samanhappy/mcphub 1.0.45 → 1.1.0

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 (78) hide show
  1. package/dist/clients/legacyMcpClient.js +30 -4
  2. package/dist/clients/legacyMcpClient.js.map +1 -1
  3. package/dist/clients/openapi.js +4 -1
  4. package/dist/clients/openapi.js.map +1 -1
  5. package/dist/controllers/oauthCallbackController.js +2 -2
  6. package/dist/controllers/oauthCallbackController.js.map +1 -1
  7. package/dist/services/mcpService.js +137 -28
  8. package/dist/services/mcpService.js.map +1 -1
  9. package/dist/services/principalRuntimeService.js +2 -2
  10. package/dist/services/principalRuntimeService.js.map +1 -1
  11. package/dist/services/proxy.js +4 -0
  12. package/dist/services/proxy.js.map +1 -1
  13. package/dist/services/serverConfigEnvironment.js +23 -0
  14. package/dist/services/serverConfigEnvironment.js.map +1 -0
  15. package/dist/services/sseService.js +11 -8
  16. package/dist/services/sseService.js.map +1 -1
  17. package/dist/utils/listFreshness.js +44 -0
  18. package/dist/utils/listFreshness.js.map +1 -0
  19. package/dist/utils/ssrf.js +49 -2
  20. package/dist/utils/ssrf.js.map +1 -1
  21. package/frontend/dist/assets/{ActivityPage-DPlbWRuE.js → ActivityPage-Dp0qfFJ7.js} +2 -2
  22. package/frontend/dist/assets/{ActivityPage-DPlbWRuE.js.map → ActivityPage-Dp0qfFJ7.js.map} +1 -1
  23. package/frontend/dist/assets/{CopyClientConfigDialog-BmrvVqKm.js → CopyClientConfigDialog-Bj0FEskD.js} +2 -2
  24. package/frontend/dist/assets/{CopyClientConfigDialog-BmrvVqKm.js.map → CopyClientConfigDialog-Bj0FEskD.js.map} +1 -1
  25. package/frontend/dist/assets/{CredentialsPage-DUa5COar.js → CredentialsPage-C-yB8aQU.js} +2 -2
  26. package/frontend/dist/assets/{CredentialsPage-DUa5COar.js.map → CredentialsPage-C-yB8aQU.js.map} +1 -1
  27. package/frontend/dist/assets/{Dashboard-DaTYK_nx.js → Dashboard-cbalSWB1.js} +2 -2
  28. package/frontend/dist/assets/{Dashboard-DaTYK_nx.js.map → Dashboard-cbalSWB1.js.map} +1 -1
  29. package/frontend/dist/assets/{EndpointCopy-BCfqwDEK.js → EndpointCopy-V8MoW7SG.js} +2 -2
  30. package/frontend/dist/assets/{EndpointCopy-BCfqwDEK.js.map → EndpointCopy-V8MoW7SG.js.map} +1 -1
  31. package/frontend/dist/assets/{GroupsPage-Cuxz5FyS.js → GroupsPage-DTXuiuqc.js} +4 -4
  32. package/frontend/dist/assets/{GroupsPage-Cuxz5FyS.js.map → GroupsPage-DTXuiuqc.js.map} +1 -1
  33. package/frontend/dist/assets/{LoginPage-yTpHn5Tx.js → LoginPage-kfbiuyWa.js} +2 -2
  34. package/frontend/dist/assets/{LoginPage-yTpHn5Tx.js.map → LoginPage-kfbiuyWa.js.map} +1 -1
  35. package/frontend/dist/assets/{LogsPage-BbSlC06Q.js → LogsPage-Bwv0shjb.js} +2 -2
  36. package/frontend/dist/assets/{LogsPage-BbSlC06Q.js.map → LogsPage-Bwv0shjb.js.map} +1 -1
  37. package/frontend/dist/assets/MarketPage-3jMYsH4H.js +2 -0
  38. package/frontend/dist/assets/MarketPage-3jMYsH4H.js.map +1 -0
  39. package/frontend/dist/assets/{OAuthConsentPage-W05BBBpL.js → OAuthConsentPage-DI-IQ2gj.js} +2 -2
  40. package/frontend/dist/assets/{OAuthConsentPage-W05BBBpL.js.map → OAuthConsentPage-DI-IQ2gj.js.map} +1 -1
  41. package/frontend/dist/assets/{PromptsPage-Bq7xptCw.js → PromptsPage-BuVTifDV.js} +2 -2
  42. package/frontend/dist/assets/{PromptsPage-Bq7xptCw.js.map → PromptsPage-BuVTifDV.js.map} +1 -1
  43. package/frontend/dist/assets/{ResourcesPage-CsL39aff.js → ResourcesPage-COSY92pD.js} +2 -2
  44. package/frontend/dist/assets/{ResourcesPage-CsL39aff.js.map → ResourcesPage-COSY92pD.js.map} +1 -1
  45. package/frontend/dist/assets/ServersPage-BlZvDfZE.js +39 -0
  46. package/frontend/dist/assets/ServersPage-BlZvDfZE.js.map +1 -0
  47. package/frontend/dist/assets/SettingsPage-V0TrX4k8.js +12 -0
  48. package/frontend/dist/assets/SettingsPage-V0TrX4k8.js.map +1 -0
  49. package/frontend/dist/assets/{StatusDot-DnCf86-T.js → StatusDot-BHfWZAic.js} +2 -2
  50. package/frontend/dist/assets/{StatusDot-DnCf86-T.js.map → StatusDot-BHfWZAic.js.map} +1 -1
  51. package/frontend/dist/assets/{ToggleGroup-DFjmIz4f.js → ToggleGroup-C33UH_Ob.js} +2 -2
  52. package/frontend/dist/assets/{ToggleGroup-DFjmIz4f.js.map → ToggleGroup-C33UH_Ob.js.map} +1 -1
  53. package/frontend/dist/assets/{UsersPage-g_A6Nis1.js → UsersPage-BXjVPFoF.js} +2 -2
  54. package/frontend/dist/assets/{UsersPage-g_A6Nis1.js.map → UsersPage-BXjVPFoF.js.map} +1 -1
  55. package/frontend/dist/assets/{clipboard-C6pDz_7o.js → clipboard-r7a9vj44.js} +2 -2
  56. package/frontend/dist/assets/{clipboard-C6pDz_7o.js.map → clipboard-r7a9vj44.js.map} +1 -1
  57. package/frontend/dist/assets/index-D-J2WUmj.css +1 -0
  58. package/frontend/dist/assets/index-pouDG4uY.js +3 -0
  59. package/frontend/dist/assets/index-pouDG4uY.js.map +1 -0
  60. package/frontend/dist/assets/{resourceService-CAxgCzyV.js → resourceService-5J2x7MrD.js} +2 -2
  61. package/frontend/dist/assets/{resourceService-CAxgCzyV.js.map → resourceService-5J2x7MrD.js.map} +1 -1
  62. package/frontend/dist/assets/{useCostData-wcUc6g7_.js → useCostData-CjmWUnd9.js} +2 -2
  63. package/frontend/dist/assets/{useCostData-wcUc6g7_.js.map → useCostData-CjmWUnd9.js.map} +1 -1
  64. package/frontend/dist/assets/variableDetection-CAv2g3qV.js +16 -0
  65. package/frontend/dist/assets/variableDetection-CAv2g3qV.js.map +1 -0
  66. package/frontend/dist/index.html +2 -2
  67. package/package.json +1 -1
  68. package/frontend/dist/assets/MarketPage-nAF8uFkv.js +0 -2
  69. package/frontend/dist/assets/MarketPage-nAF8uFkv.js.map +0 -1
  70. package/frontend/dist/assets/ServersPage-p9LgWUzS.js +0 -39
  71. package/frontend/dist/assets/ServersPage-p9LgWUzS.js.map +0 -1
  72. package/frontend/dist/assets/SettingsPage-DobkN2nZ.js +0 -12
  73. package/frontend/dist/assets/SettingsPage-DobkN2nZ.js.map +0 -1
  74. package/frontend/dist/assets/index-BlEZfPQl.js +0 -3
  75. package/frontend/dist/assets/index-BlEZfPQl.js.map +0 -1
  76. package/frontend/dist/assets/index-Cla8rRxG.css +0 -1
  77. package/frontend/dist/assets/variableDetection-DxVZhKpb.js +0 -16
  78. package/frontend/dist/assets/variableDetection-DxVZhKpb.js.map +0 -1
@@ -1,3 +1,5 @@
1
+ import { preserveListFreshness, combineListFreshness, remainingListTtl, } from '../utils/listFreshness.js';
2
+ import { expandServerConfig, getServerEnvironment } from './serverConfigEnvironment.js';
1
3
  import { getMcpRequestGroup } from '../utils/mcpRequestGroup.js';
2
4
  import { LegacyMcpClient } from '../clients/legacyMcpClient.js';
3
5
  import { LEGACY_PROTOCOL_VERSIONS } from '../utils/mcpProtocol.js';
@@ -29,7 +31,7 @@ import { removeServerToolEmbeddings, removeToolEmbeddings, saveToolsAsVectorEmbe
29
31
  import { OpenAPIClient } from '../clients/openapi.js';
30
32
  import { RequestContextService } from './requestContextService.js';
31
33
  import { getDataService } from './services.js';
32
- import { getServerDao, getGroupDao, getSystemConfigDao, getBuiltinPromptDao, getBuiltinResourceDao, } from '../dao/index.js';
34
+ import { getBearerKeyDao, getServerDao, getGroupDao, getSystemConfigDao, getBuiltinPromptDao, getBuiltinResourceDao, } from '../dao/index.js';
33
35
  import { initializeAllOAuthClients } from './oauthService.js';
34
36
  import { createOAuthProvider } from './mcpOAuthProvider.js';
35
37
  import { initSmartRoutingService, getSmartRoutingTools, handleSearchToolsRequest, handleDescribeToolRequest, isSmartRoutingGroup, } from './smartRoutingService.js';
@@ -781,7 +783,7 @@ export const setServerInfosForTest = (infos) => {
781
783
  serverInfos = infos;
782
784
  };
783
785
  export const updateServerToolsCache = (serverInfo, tools, options) => {
784
- serverInfo.tools = tools.map((tool) => normalizeToolForCache(serverInfo.name, tool));
786
+ serverInfo.tools = preserveListFreshness(tools, tools.map((tool) => normalizeToolForCache(serverInfo.name, tool)));
785
787
  syncToolsAsVectorEmbeddings(serverInfo.name, serverInfo.tools, {
786
788
  reportProgress: options?.reportEmbeddingProgress === true,
787
789
  }).catch(() => {
@@ -789,10 +791,10 @@ export const updateServerToolsCache = (serverInfo, tools, options) => {
789
791
  });
790
792
  };
791
793
  const updateServerPromptsCache = (serverInfo, prompts) => {
792
- serverInfo.prompts = prompts.map((prompt) => normalizePromptForCache(serverInfo.name, prompt));
794
+ serverInfo.prompts = preserveListFreshness(prompts, prompts.map((prompt) => normalizePromptForCache(serverInfo.name, prompt)));
793
795
  };
794
796
  const updateServerResourcesCache = (serverInfo, resources) => {
795
- serverInfo.resources = resources.map(normalizeResourceForCache);
797
+ serverInfo.resources = preserveListFreshness(resources, resources.map(normalizeResourceForCache));
796
798
  };
797
799
  const logListChangedRefreshError = (listType) => {
798
800
  logger.warn(`Failed to refresh ${listType} list after upstream notification`);
@@ -806,6 +808,7 @@ const createUpstreamMcpClient = (name, getServerInfo) => {
806
808
  jsonSchemaValidator: new ResilientJsonSchemaValidator(),
807
809
  listChanged: {
808
810
  tools: {
811
+ debounceMs: 0,
809
812
  onChanged: (error, tools) => {
810
813
  const serverInfo = getServerInfo();
811
814
  if (error) {
@@ -820,6 +823,7 @@ const createUpstreamMcpClient = (name, getServerInfo) => {
820
823
  },
821
824
  },
822
825
  prompts: {
826
+ debounceMs: 0,
823
827
  onChanged: (error, prompts) => {
824
828
  const serverInfo = getServerInfo();
825
829
  if (error) {
@@ -834,6 +838,7 @@ const createUpstreamMcpClient = (name, getServerInfo) => {
834
838
  },
835
839
  },
836
840
  resources: {
841
+ debounceMs: 0,
837
842
  onChanged: (error, resources) => {
838
843
  const serverInfo = getServerInfo();
839
844
  if (error) {
@@ -1171,10 +1176,7 @@ const stripAuthorizationHeader = (headers) => {
1171
1176
  };
1172
1177
  export const createTransportFromConfig = async (name, conf) => {
1173
1178
  let transport;
1174
- const env = {
1175
- ...process.env,
1176
- ...(hasCredentialTemplate(conf) ? conf.env : replaceEnvVars(conf.env || {})),
1177
- };
1179
+ const env = await getServerEnvironment(conf);
1178
1180
  // The encryption key belongs only to the hub, never to a child process.
1179
1181
  delete env.MCPHUB_CREDENTIAL_ENCRYPTION_KEY;
1180
1182
  // SSRF guard: block URL/streamable-http transports from reaching
@@ -1275,7 +1277,7 @@ export const createTransportFromConfig = async (name, conf) => {
1275
1277
  env['npm_config_registry'] = systemConfig.install.npmRegistry;
1276
1278
  }
1277
1279
  // Apply proxychains4 wrapper if proxy is configured (Linux/macOS only)
1278
- let resolvedArgs = replaceEnvVars(conf.args ?? []);
1280
+ let resolvedArgs = replaceEnvVars(conf.args ?? [], env);
1279
1281
  // If this server is pending a reinstall, inject cache-busting flags (uvx only).
1280
1282
  // For npx, the cache directory was already cleared before reconnect.
1281
1283
  if (pendingReinstalls.has(name)) {
@@ -1344,7 +1346,7 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
1344
1346
  });
1345
1347
  }
1346
1348
  // Match initial connection expansion before URL validation and transport creation.
1347
- const server = replaceEnvVars(rawConfig);
1349
+ const server = await expandServerConfig(rawConfig);
1348
1350
  const newTransport = await createTransportFromConfig(serverInfo.name, server);
1349
1351
  const newClient = createUpstreamMcpClient(serverInfo.name, () => serverInfo);
1350
1352
  // Reconnect with new transport
@@ -1476,7 +1478,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
1476
1478
  }
1477
1479
  }
1478
1480
  // Expand environment variables in all configuration values
1479
- const expandedConf = replaceEnvVars(conf);
1481
+ const expandedConf = await expandServerConfig(conf);
1480
1482
  // Skip disabled servers
1481
1483
  if (expandedConf.enabled === false) {
1482
1484
  logger.log(`Skipping disabled server: ${name}`);
@@ -2542,7 +2544,7 @@ const ensureServerReady = async (serverInfo) => {
2542
2544
  if (rawConfig.enabled === false) {
2543
2545
  throw new Error(`Cannot start disabled on-demand server: ${rawConfig.name}`);
2544
2546
  }
2545
- const expandedConf = replaceEnvVars(rawConfig);
2547
+ const expandedConf = await expandServerConfig(rawConfig);
2546
2548
  // startOnDemand is a stdio-only optimisation; OpenAPI/HTTP servers have no
2547
2549
  // deferred process to spawn, so there is nothing to wake.
2548
2550
  if (expandedConf.type === 'openapi') {
@@ -3766,19 +3768,24 @@ const handleListResourceTemplatesRequestImpl = async (_, extra) => {
3766
3768
  const { filteredServerInfos, serverConfigsByName } = await getFilteredServerInfosForGroup(lookupGroup, {
3767
3769
  requireClient: true,
3768
3770
  });
3771
+ const snapshots = [];
3769
3772
  const results = await Promise.allSettled(filteredServerInfos.map(async (serverInfo) => {
3770
3773
  if (!serverInfo.client?.listResourceTemplates) {
3771
3774
  return [];
3772
3775
  }
3773
3776
  const templates = await serverInfo.client.listResourceTemplates({}, serverInfo.options || {});
3777
+ snapshots.push(templates.resourceTemplates);
3774
3778
  const filteredTemplates = await filterResourceTemplatesByGroup(lookupGroup, serverInfo.name, templates.resourceTemplates || [], serverConfigsByName.get(serverInfo.name));
3775
3779
  return appsRouteContext.enabled
3776
3780
  ? filteredTemplates
3777
3781
  : filteredTemplates.map((template) => stripMcpAppsMetadata(template));
3778
3782
  }));
3779
- return {
3780
- resourceTemplates: results.flatMap((result) => result.status === 'fulfilled' ? result.value : []),
3781
- };
3783
+ const resourceTemplates = results.flatMap((result) => result.status === 'fulfilled' ? result.value : []);
3784
+ if (results.every((result) => result.status === 'fulfilled') &&
3785
+ snapshots.length === filteredServerInfos.length) {
3786
+ combineListFreshness(snapshots, resourceTemplates);
3787
+ }
3788
+ return { resourceTemplates };
3782
3789
  };
3783
3790
  const handleReadResourceRequestImpl = async (request, extra) => {
3784
3791
  try {
@@ -3818,8 +3825,15 @@ const handleReadResourceRequestImpl = async (request, extra) => {
3818
3825
  }
3819
3826
  }
3820
3827
  let result;
3828
+ // A cached SDK body carries its original TTL, not the remaining lifetime.
3829
+ // Fetch modern reads afresh so forwarding the TTL cannot renew stale content.
3830
+ const readOptions = RequestContextService.getInstance().getRequestContext()?.stateless
3831
+ ? { cacheMode: 'bypass' }
3832
+ : undefined;
3821
3833
  if (server?.client) {
3822
- result = await server.client.readResource({ uri });
3834
+ result = readOptions
3835
+ ? await server.client.readResource({ uri }, readOptions)
3836
+ : await server.client.readResource({ uri });
3823
3837
  if (!result || !Array.isArray(result.contents)) {
3824
3838
  throw new Error(`Failed to read resource: ${uri}`);
3825
3839
  }
@@ -3839,7 +3853,9 @@ const handleReadResourceRequestImpl = async (request, extra) => {
3839
3853
  continue;
3840
3854
  }
3841
3855
  try {
3842
- const candidateResult = await candidate.client.readResource({ uri });
3856
+ const candidateResult = readOptions
3857
+ ? await candidate.client.readResource({ uri }, readOptions)
3858
+ : await candidate.client.readResource({ uri });
3843
3859
  if (candidateResult && Array.isArray(candidateResult.contents)) {
3844
3860
  result = candidateResult;
3845
3861
  break;
@@ -3947,12 +3963,18 @@ const createPrincipalRuntime = async (name, resolvedConfig) => {
3947
3963
  info.client = protectClient(client);
3948
3964
  await client.connect(transport, resolvedConfig.options || {});
3949
3965
  const capabilities = client.getServerCapabilities();
3950
- if (capabilities?.tools)
3951
- info.tools = (await client.listTools({}, resolvedConfig.options)).tools.map((tool) => normalizeToolForCache(name, tool));
3952
- if (capabilities?.prompts)
3953
- info.prompts = (await client.listPrompts({}, resolvedConfig.options)).prompts.map((prompt) => normalizePromptForCache(name, prompt));
3954
- if (capabilities?.resources)
3955
- info.resources = (await client.listResources({}, resolvedConfig.options)).resources.map(normalizeResourceForCache);
3966
+ if (capabilities?.tools) {
3967
+ const { tools } = await client.listTools({}, resolvedConfig.options);
3968
+ info.tools = preserveListFreshness(tools, tools.map((tool) => normalizeToolForCache(name, tool)));
3969
+ }
3970
+ if (capabilities?.prompts) {
3971
+ const { prompts } = await client.listPrompts({}, resolvedConfig.options);
3972
+ info.prompts = preserveListFreshness(prompts, prompts.map((prompt) => normalizePromptForCache(name, prompt)));
3973
+ }
3974
+ if (capabilities?.resources) {
3975
+ const { resources } = await client.listResources({}, resolvedConfig.options);
3976
+ info.resources = preserveListFreshness(resources, resources.map(normalizeResourceForCache));
3977
+ }
3956
3978
  info.version = client.getServerVersion()?.version;
3957
3979
  // Record the resolved npx/uvx package version backing this server.
3958
3980
  applyResolvedPackageVersion(info, resolvedConfig);
@@ -4104,13 +4126,100 @@ const withPrincipalServers = (handler, operation) => async (request, extra) => {
4104
4126
  release();
4105
4127
  }
4106
4128
  };
4107
- export const handleListToolsRequest = withPrincipalServers(handleListToolsRequestImpl, 'list');
4129
+ // Re-read authoritative inputs across awaits; no projected response is cached.
4130
+ const listProjectionInputs = async (group, field) => {
4131
+ const user = UserContextService.getInstance().getCurrentUser();
4132
+ const keyId = RequestContextService.getInstance().getBearerKeyContext().keyId;
4133
+ const liveUser = user?.username ? await getUserDao().findByUsername(user.username) : undefined;
4134
+ const key = keyId ? await getBearerKeyDao().findById(keyId) : undefined;
4135
+ if ((user?.username && (!liveUser || Boolean(user.isAdmin) !== Boolean(liveUser.isAdmin))) ||
4136
+ (keyId && !key?.enabled))
4137
+ return undefined;
4138
+ const lookupGroup = getGroupLookupName(group);
4139
+ const groupDao = getGroupDao();
4140
+ let routeGroup = lookupGroup ? await groupDao.findByName(lookupGroup) : undefined;
4141
+ if (!routeGroup &&
4142
+ lookupGroup &&
4143
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(lookupGroup)) {
4144
+ routeGroup = await groupDao.findById(lookupGroup);
4145
+ }
4146
+ return JSON.stringify([
4147
+ await getServerDao().findAll(),
4148
+ routeGroup,
4149
+ await getSystemConfigDao().get(),
4150
+ field === 'prompts' ? await getBuiltinPromptDao().findEnabled() : undefined,
4151
+ field === 'resources' ? await getBuiltinResourceDao().findEnabled() : undefined,
4152
+ liveUser,
4153
+ key,
4154
+ RequestContextService.getInstance().getRequestContext()?.clientCapabilities,
4155
+ getVisibleServerInfos().map((info) => ({
4156
+ name: info.name,
4157
+ status: info.status,
4158
+ enabled: info.enabled,
4159
+ owner: info.owner,
4160
+ visibility: info.visibility,
4161
+ sharedWithUsers: info.sharedWithUsers,
4162
+ config: info.config,
4163
+ })),
4164
+ ]);
4165
+ };
4166
+ // Lists combine cached discovery with live gateway configuration and authorization.
4167
+ // Upstream freshness alone cannot guarantee freshness of the resulting projection.
4168
+ const withCacheHints = (handler, passthroughTtl = false, listField) => async (request, extra) => {
4169
+ if (!RequestContextService.getInstance().getRequestContext()?.stateless) {
4170
+ return handler(request, extra);
4171
+ }
4172
+ const startedAt = performance.now();
4173
+ const group = getMcpRequestGroup(extra);
4174
+ const inputs = listField ? await listProjectionInputs(group, listField) : undefined;
4175
+ const before = listField
4176
+ ? await getFilteredServerInfosForGroup(getGroupLookupName(group))
4177
+ : undefined;
4178
+ const snapshots = before && listField && listField !== 'resourceTemplates'
4179
+ ? before.filteredServerInfos.map((info) => info[listField])
4180
+ : [];
4181
+ const clients = before?.filteredServerInfos.map((info) => info.client);
4182
+ const result = await handler(request, extra);
4183
+ if (listField) {
4184
+ const after = await getFilteredServerInfosForGroup(getGroupLookupName(group));
4185
+ const stable = typeof inputs === 'string' &&
4186
+ inputs === (await listProjectionInputs(group, listField)) &&
4187
+ before?.filteredServerInfos.length === after.filteredServerInfos.length &&
4188
+ before.filteredServerInfos.every((info, index) => info === after.filteredServerInfos[index] &&
4189
+ info.client === clients?.[index] &&
4190
+ info.status === 'connected' &&
4191
+ (listField === 'resourceTemplates' || info[listField] === snapshots[index]));
4192
+ const generated = (listField === 'tools' && isSmartRoutingGroup(group)) ||
4193
+ (listField === 'prompts' && (await getBuiltinPromptDao().findEnabled()).length > 0) ||
4194
+ (listField === 'resources' && (await getBuiltinResourceDao().findEnabled()).length > 0);
4195
+ const unknownPermissions = RequestContextService.getInstance().getHostedAuthContext();
4196
+ const dependencies = listField === 'resourceTemplates' ? [result.resourceTemplates] : snapshots;
4197
+ return {
4198
+ ...result,
4199
+ ttlMs: stable && !generated && !unknownPermissions
4200
+ ? remainingListTtl(dependencies, startedAt, performance.now())
4201
+ : 0,
4202
+ cacheScope: 'private',
4203
+ };
4204
+ }
4205
+ const upstreamTtl = result.ttlMs;
4206
+ const ttlMs = passthroughTtl &&
4207
+ typeof upstreamTtl === 'number' &&
4208
+ Number.isSafeInteger(upstreamTtl) &&
4209
+ upstreamTtl >= 0
4210
+ ? Math.max(0, upstreamTtl - Math.ceil(performance.now() - startedAt))
4211
+ : 0;
4212
+ // Even public upstream content is projected through caller-specific permissions
4213
+ // and capabilities. Shared caches must not reuse the gateway result across users.
4214
+ return { ...result, ttlMs, cacheScope: 'private' };
4215
+ };
4216
+ export const handleListToolsRequest = withPrincipalServers(withCacheHints(handleListToolsRequestImpl, false, 'tools'), 'list');
4108
4217
  export const handleCallToolRequest = withPrincipalServers(handleCallToolRequestImpl, 'tool');
4109
4218
  export const handleGetPromptRequest = withPrincipalServers(handleGetPromptRequestImpl, 'prompt');
4110
- export const handleListPromptsRequest = withPrincipalServers(handleListPromptsRequestImpl, 'list');
4111
- export const handleListResourcesRequest = withPrincipalServers(handleListResourcesRequestImpl, 'list');
4112
- export const handleListResourceTemplatesRequest = withPrincipalServers(handleListResourceTemplatesRequestImpl, 'list');
4113
- export const handleReadResourceRequest = withPrincipalServers(handleReadResourceRequestImpl, 'resource');
4219
+ export const handleListPromptsRequest = withPrincipalServers(withCacheHints(handleListPromptsRequestImpl, false, 'prompts'), 'list');
4220
+ export const handleListResourcesRequest = withPrincipalServers(withCacheHints(handleListResourcesRequestImpl, false, 'resources'), 'list');
4221
+ export const handleListResourceTemplatesRequest = withPrincipalServers(withCacheHints(handleListResourceTemplatesRequestImpl, false, 'resourceTemplates'), 'list');
4222
+ export const handleReadResourceRequest = withPrincipalServers(withCacheHints(handleReadResourceRequestImpl, true), 'resource');
4114
4223
  export const createMcpServer = (name, version, options) => {
4115
4224
  const normalizedOptions = typeof options === 'string' ? { group: options } : (options ?? {});
4116
4225
  // Determine server name based on routing type