@samanhappy/mcphub 1.0.38 → 1.0.40

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 (130) hide show
  1. package/dist/betterAuth.js +23 -0
  2. package/dist/betterAuth.js.map +1 -1
  3. package/dist/controllers/smartRoutingController.js +408 -0
  4. package/dist/controllers/smartRoutingController.js.map +1 -0
  5. package/dist/dao/CredentialBindingDao.js +5 -0
  6. package/dist/dao/CredentialBindingDao.js.map +1 -1
  7. package/dist/dao/CredentialBindingDaoDbImpl.js +7 -0
  8. package/dist/dao/CredentialBindingDaoDbImpl.js.map +1 -1
  9. package/dist/db/connection.js +38 -11
  10. package/dist/db/connection.js.map +1 -1
  11. package/dist/db/groupSchema.js +2 -2
  12. package/dist/db/groupSchema.js.map +1 -1
  13. package/dist/db/repositories/ActivityRepository.js +2 -2
  14. package/dist/db/repositories/ActivityRepository.js.map +1 -1
  15. package/dist/db/repositories/BaseRepository.js +4 -1
  16. package/dist/db/repositories/BaseRepository.js.map +1 -1
  17. package/dist/db/repositories/BearerKeyRepository.js +3 -1
  18. package/dist/db/repositories/BearerKeyRepository.js.map +1 -1
  19. package/dist/db/repositories/BuiltinPromptRepository.js +2 -2
  20. package/dist/db/repositories/BuiltinPromptRepository.js.map +1 -1
  21. package/dist/db/repositories/BuiltinResourceRepository.js +2 -2
  22. package/dist/db/repositories/BuiltinResourceRepository.js.map +1 -1
  23. package/dist/db/repositories/GroupRepository.js +2 -2
  24. package/dist/db/repositories/GroupRepository.js.map +1 -1
  25. package/dist/db/repositories/OAuthClientRepository.js +2 -2
  26. package/dist/db/repositories/OAuthClientRepository.js.map +1 -1
  27. package/dist/db/repositories/OAuthTokenRepository.js +3 -1
  28. package/dist/db/repositories/OAuthTokenRepository.js.map +1 -1
  29. package/dist/db/repositories/ServerRepository.js +2 -2
  30. package/dist/db/repositories/ServerRepository.js.map +1 -1
  31. package/dist/db/repositories/UserConfigRepository.js +2 -2
  32. package/dist/db/repositories/UserConfigRepository.js.map +1 -1
  33. package/dist/db/repositories/UserRepository.js +3 -1
  34. package/dist/db/repositories/UserRepository.js.map +1 -1
  35. package/dist/db/repositories/VectorEmbeddingRepository.js +35 -3
  36. package/dist/db/repositories/VectorEmbeddingRepository.js.map +1 -1
  37. package/dist/middlewares/auth.js +1 -1
  38. package/dist/middlewares/auth.js.map +1 -1
  39. package/dist/middlewares/index.js +8 -1
  40. package/dist/middlewares/index.js.map +1 -1
  41. package/dist/routes/index.js +4 -0
  42. package/dist/routes/index.js.map +1 -1
  43. package/dist/services/mcpOAuthProvider.js +7 -0
  44. package/dist/services/mcpOAuthProvider.js.map +1 -1
  45. package/dist/services/mcpService.js +178 -20
  46. package/dist/services/mcpService.js.map +1 -1
  47. package/dist/services/principalRuntimeService.js +1 -3
  48. package/dist/services/principalRuntimeService.js.map +1 -1
  49. package/dist/services/vectorSearchService.js +29 -10
  50. package/dist/services/vectorSearchService.js.map +1 -1
  51. package/dist/utils/cacheUtils.js +345 -1
  52. package/dist/utils/cacheUtils.js.map +1 -1
  53. package/dist/utils/packageUpdate.js +242 -0
  54. package/dist/utils/packageUpdate.js.map +1 -0
  55. package/dist/utils/serialization.js +14 -2
  56. package/dist/utils/serialization.js.map +1 -1
  57. package/dist/utils/serverConfigPersistence.js +18 -0
  58. package/dist/utils/serverConfigPersistence.js.map +1 -1
  59. package/dist/utils/upstreamError.js +60 -0
  60. package/dist/utils/upstreamError.js.map +1 -0
  61. package/dist/utils/withTimeout.js +25 -0
  62. package/dist/utils/withTimeout.js.map +1 -0
  63. package/frontend/dist/assets/{ActivityPage-BZNq-eGE.js → ActivityPage-B5DanW8q.js} +2 -2
  64. package/frontend/dist/assets/{ActivityPage-BZNq-eGE.js.map → ActivityPage-B5DanW8q.js.map} +1 -1
  65. package/frontend/dist/assets/CopyClientConfigDialog-C6U6_eyy.js +4 -0
  66. package/frontend/dist/assets/CopyClientConfigDialog-C6U6_eyy.js.map +1 -0
  67. package/frontend/dist/assets/{CredentialsPage-7kyDJLsX.js → CredentialsPage-CXK8Xkao.js} +2 -2
  68. package/frontend/dist/assets/{CredentialsPage-7kyDJLsX.js.map → CredentialsPage-CXK8Xkao.js.map} +1 -1
  69. package/frontend/dist/assets/Dashboard-LIC6BqIA.js +2 -0
  70. package/frontend/dist/assets/{Dashboard-CElNx820.js.map → Dashboard-LIC6BqIA.js.map} +1 -1
  71. package/frontend/dist/assets/EndpointCopy-CeZbEsF2.js +2 -0
  72. package/frontend/dist/assets/EndpointCopy-CeZbEsF2.js.map +1 -0
  73. package/frontend/dist/assets/GroupsPage-DwVaRIap.js +33 -0
  74. package/frontend/dist/assets/GroupsPage-DwVaRIap.js.map +1 -0
  75. package/frontend/dist/assets/{LoginPage-D23s7oM0.js → LoginPage-CPqSMwWO.js} +2 -2
  76. package/frontend/dist/assets/{LoginPage-D23s7oM0.js.map → LoginPage-CPqSMwWO.js.map} +1 -1
  77. package/frontend/dist/assets/{LogsPage-BuQMkmV-.js → LogsPage-eCHGem9T.js} +2 -2
  78. package/frontend/dist/assets/{LogsPage-BuQMkmV-.js.map → LogsPage-eCHGem9T.js.map} +1 -1
  79. package/frontend/dist/assets/{MarketPage-g9vEYzIq.js → MarketPage-RqD2-rxq.js} +2 -2
  80. package/frontend/dist/assets/{MarketPage-g9vEYzIq.js.map → MarketPage-RqD2-rxq.js.map} +1 -1
  81. package/frontend/dist/assets/{OAuthConsentPage-BiX1lpJR.js → OAuthConsentPage-DJITYpCM.js} +2 -2
  82. package/frontend/dist/assets/{OAuthConsentPage-BiX1lpJR.js.map → OAuthConsentPage-DJITYpCM.js.map} +1 -1
  83. package/frontend/dist/assets/{PromptsPage-g8SRFY6s.js → PromptsPage-DQ02iDOJ.js} +2 -2
  84. package/frontend/dist/assets/{PromptsPage-g8SRFY6s.js.map → PromptsPage-DQ02iDOJ.js.map} +1 -1
  85. package/frontend/dist/assets/{ResourcesPage-Ddhxi9Sf.js → ResourcesPage-mUU0DVus.js} +2 -2
  86. package/frontend/dist/assets/{ResourcesPage-Ddhxi9Sf.js.map → ResourcesPage-mUU0DVus.js.map} +1 -1
  87. package/frontend/dist/assets/ServersPage-DsS8j6tl.js +39 -0
  88. package/frontend/dist/assets/ServersPage-DsS8j6tl.js.map +1 -0
  89. package/frontend/dist/assets/SettingsPage-CIeF1sDE.js +12 -0
  90. package/frontend/dist/assets/SettingsPage-CIeF1sDE.js.map +1 -0
  91. package/frontend/dist/assets/{StatusDot-CdzMqjTi.js → StatusDot-Bcv2BwIL.js} +2 -2
  92. package/frontend/dist/assets/{StatusDot-CdzMqjTi.js.map → StatusDot-Bcv2BwIL.js.map} +1 -1
  93. package/frontend/dist/assets/{ToggleGroup-CJd8X_31.js → ToggleGroup-C_7sDUlr.js} +2 -2
  94. package/frontend/dist/assets/{ToggleGroup-CJd8X_31.js.map → ToggleGroup-C_7sDUlr.js.map} +1 -1
  95. package/frontend/dist/assets/{UsersPage-CkP_x9Rt.js → UsersPage-ufaG2-Ag.js} +2 -2
  96. package/frontend/dist/assets/{UsersPage-CkP_x9Rt.js.map → UsersPage-ufaG2-Ag.js.map} +1 -1
  97. package/frontend/dist/assets/clipboard-BukcCPLt.js +2 -0
  98. package/frontend/dist/assets/clipboard-BukcCPLt.js.map +1 -0
  99. package/frontend/dist/assets/{icons-vendor-VW-UwEhw.js → icons-vendor-CNEaJWqW.js} +79 -59
  100. package/frontend/dist/assets/icons-vendor-CNEaJWqW.js.map +1 -0
  101. package/frontend/dist/assets/index-C8aAOWR7.js +3 -0
  102. package/frontend/dist/assets/{index-Bu4XuAH4.js.map → index-C8aAOWR7.js.map} +1 -1
  103. package/frontend/dist/assets/index-D4Vy78bV.css +1 -0
  104. package/frontend/dist/assets/{resourceService-w-cZ31FD.js → resourceService-CN5cMjQ0.js} +2 -2
  105. package/frontend/dist/assets/{resourceService-w-cZ31FD.js.map → resourceService-CN5cMjQ0.js.map} +1 -1
  106. package/frontend/dist/assets/{useCostData-C72NC0PQ.js → useCostData-B9XzWVsV.js} +2 -2
  107. package/frontend/dist/assets/{useCostData-C72NC0PQ.js.map → useCostData-B9XzWVsV.js.map} +1 -1
  108. package/frontend/dist/assets/useSettingsData-J_hwKZA6.js +2 -0
  109. package/frontend/dist/assets/{useSettingsData-JTEMu74D.js.map → useSettingsData-J_hwKZA6.js.map} +1 -1
  110. package/frontend/dist/assets/variableDetection-Dxw0g2ON.js +16 -0
  111. package/frontend/dist/assets/variableDetection-Dxw0g2ON.js.map +1 -0
  112. package/frontend/dist/index.html +3 -3
  113. package/package.json +2 -1
  114. package/frontend/dist/assets/Dashboard-CElNx820.js +0 -2
  115. package/frontend/dist/assets/EndpointCopy-8jW5Xtka.js +0 -2
  116. package/frontend/dist/assets/EndpointCopy-8jW5Xtka.js.map +0 -1
  117. package/frontend/dist/assets/GroupsPage-dUkvX_iB.js +0 -33
  118. package/frontend/dist/assets/GroupsPage-dUkvX_iB.js.map +0 -1
  119. package/frontend/dist/assets/ServersPage-DAmKvrPV.js +0 -39
  120. package/frontend/dist/assets/ServersPage-DAmKvrPV.js.map +0 -1
  121. package/frontend/dist/assets/SettingsPage-DZXjQZXS.js +0 -12
  122. package/frontend/dist/assets/SettingsPage-DZXjQZXS.js.map +0 -1
  123. package/frontend/dist/assets/icons-vendor-VW-UwEhw.js.map +0 -1
  124. package/frontend/dist/assets/index-B3OqAhGz.css +0 -1
  125. package/frontend/dist/assets/index-Bu4XuAH4.js +0 -3
  126. package/frontend/dist/assets/toolDescription-Dw3sa28K.js +0 -2
  127. package/frontend/dist/assets/toolDescription-Dw3sa28K.js.map +0 -1
  128. package/frontend/dist/assets/useSettingsData-JTEMu74D.js +0 -2
  129. package/frontend/dist/assets/variableDetection-DfhNyFy-.js +0 -16
  130. package/frontend/dist/assets/variableDetection-DfhNyFy-.js.map +0 -1
@@ -40,9 +40,11 @@ import { assertHostedToolAllowed, filterHostedTools, reserveHostedToolCall, sett
40
40
  import { formatErrorForLogging, sanitizeStringForLogging, summarizeErrorForLogging, } from '../utils/serialization.js';
41
41
  import { addStdioErrorContext, observeStdioStderr } from '../utils/stdioDiagnostics.js';
42
42
  import { MCP_APPS_CAPABILITIES, filterModelVisibleTools, hasMcpAppsCapability, isAppOnlyTool, stripMcpAppsMetadata, } from '../utils/mcpApps.js';
43
- import { supportsCacheRefresh, injectRefreshFlag, clearRunnerCache } from '../utils/cacheUtils.js';
43
+ import { supportsCacheRefresh, injectRefreshFlag, clearRunnerCache, resolveRunnerPackageVersion } from '../utils/cacheUtils.js';
44
+ import { checkPackageUpdate } from '../utils/packageUpdate.js';
44
45
  const servers = {};
45
46
  import { setupClientKeepAlive } from './keepAliveService.js';
47
+ import { createUpstreamRequestError, isUpstreamConnectionFailure, UpstreamRequestError, } from '../utils/upstreamError.js';
46
48
  import { logger } from '../utils/logger.js';
47
49
  /**
48
50
  * Check if proxychains4 is available on the system (Linux/macOS only).
@@ -439,7 +441,12 @@ export const syncToolEmbedding = async (serverName, toolName) => {
439
441
  logger.warn(`Server not found: ${serverName}`);
440
442
  return;
441
443
  }
442
- const tool = serverInfo.tools.find((t) => t.name === toolName);
444
+ // The runtime cache names tools with the server prefix, but a direct API call
445
+ // (or a client that only knows the upstream name) may pass the bare name.
446
+ // Match both, mirroring the dual lookup used for the disabled-tool check, so a
447
+ // description update always reaches the right tool (see #1198).
448
+ const tool = serverInfo.tools.find((t) => t.name === toolName) ??
449
+ serverInfo.tools.find((t) => normalizeToolNameForServer(serverInfo.name, t.name) === toolName);
443
450
  if (!tool) {
444
451
  logger.warn(`Tool not found: ${toolName} on server: ${serverName}`);
445
452
  return;
@@ -448,7 +455,7 @@ export const syncToolEmbedding = async (serverName, toolName) => {
448
455
  return;
449
456
  }
450
457
  // Save tool as vector embedding for search
451
- syncToolsAsVectorEmbeddings(serverName, [tool]).catch((error) => {
458
+ syncToolsAsVectorEmbeddings(serverName, [tool], { partial: true }).catch((error) => {
452
459
  logger.warn(`[EMBED_SYNC_ERROR] Failed to sync embedding for tool "${toolName}" on server "${serverName}"`);
453
460
  logger.error('Error syncing single tool embedding', { serverName, toolName, error });
454
461
  });
@@ -486,9 +493,38 @@ const buildToolWithDescriptionMetadata = (tool, toolConfig) => {
486
493
  enabled: toolConfig?.enabled !== false,
487
494
  };
488
495
  };
496
+ // Apply any per-tool description override before the tool text is embedded for
497
+ // Smart Routing search. Deliberately narrower than buildToolWithDescriptionMetadata:
498
+ // only `description` (plus the hasDescriptionOverride marker used by the
499
+ // tool-set hash) is touched, so serverInfo.tools (built by
500
+ // normalizeToolForCache/updateServerToolsCache) still never carries `enabled` --
501
+ // preserving the existing invariant that execution gating reads the persisted
502
+ // config directly, not this projection. Without this step, overrides only
503
+ // affected client-facing output (tools/list, describe_tool, dashboard) while
504
+ // search_tools embeddings kept ranking against the raw upstream description (#1198).
505
+ const applyDescriptionOverridesForEmbedding = async (serverName, tools) => {
506
+ const serverConfig = await getServerDao().findById(serverName);
507
+ if (!serverConfig?.tools) {
508
+ return tools;
509
+ }
510
+ return tools.map((tool) => {
511
+ const bareToolName = normalizeToolNameForServer(serverName, tool.name);
512
+ const toolConfig = serverConfig.tools?.[tool.name] ?? serverConfig.tools?.[bareToolName];
513
+ return hasDescriptionOverride(toolConfig)
514
+ ? {
515
+ ...tool,
516
+ description: resolveDescriptionOverride(tool.description, toolConfig),
517
+ hasDescriptionOverride: true,
518
+ }
519
+ : tool;
520
+ });
521
+ };
489
522
  const syncToolsAsVectorEmbeddings = async (serverName, tools, options) => {
490
- const modelVisibleTools = filterModelVisibleTools(tools);
523
+ const toolsWithOverrides = await applyDescriptionOverridesForEmbedding(serverName, tools);
524
+ const modelVisibleTools = filterModelVisibleTools(toolsWithOverrides);
491
525
  if (modelVisibleTools.length === 0) {
526
+ if (options?.partial)
527
+ return;
492
528
  await removeServerToolEmbeddings(serverName);
493
529
  return;
494
530
  }
@@ -754,6 +790,7 @@ export const connected = () => {
754
790
  };
755
791
  // Global cleanup function to close all connections
756
792
  export const cleanupAllServers = () => {
793
+ stopPackageUpdateSweep();
757
794
  principalRuntimes.invalidate();
758
795
  for (const serverInfo of serverInfos) {
759
796
  try {
@@ -1625,6 +1662,8 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
1625
1662
  const serverVersion = client.getServerVersion?.();
1626
1663
  info.version = serverVersion?.version;
1627
1664
  info.instructions = client.getInstructions?.();
1665
+ // Record the resolved npx/uvx package version backing this server.
1666
+ applyResolvedPackageVersion(info, expandedConf);
1628
1667
  const capabilities = client.getServerCapabilities();
1629
1668
  logger.log('Server capabilities', JSON.stringify(capabilities));
1630
1669
  let dataError = null;
@@ -1839,15 +1878,15 @@ export const getServersInfo = async (page, limit, user) => {
1839
1878
  catch (error) {
1840
1879
  return {
1841
1880
  ...info,
1842
- error: error instanceof CredentialBindingError
1881
+ error: error instanceof CredentialBindingError || error instanceof UpstreamRequestError
1843
1882
  ? error.message
1844
- : 'Unable to resolve personal credentials',
1883
+ : 'Unable to prepare upstream server',
1845
1884
  };
1846
1885
  }
1847
1886
  }));
1848
1887
  const infos = presentedInfos
1849
1888
  .filter((info) => requestedServerNames.has(info.name)) // Only include requested servers
1850
- .map(({ name, version, instructions, owner, visibility, status, tools, prompts, resources, createTime, error, oauth, }) => {
1889
+ .map(({ name, version, packageVersion, latestVersion, updateAvailable, instructions, owner, visibility, status, tools, prompts, resources, createTime, error, oauth, }) => {
1851
1890
  const serverConfig = allServers.find((server) => server.name === name);
1852
1891
  const enabled = serverConfig ? serverConfig.enabled !== false : true;
1853
1892
  const resolvedType = inferServerType(serverConfig);
@@ -1891,6 +1930,12 @@ export const getServersInfo = async (page, limit, user) => {
1891
1930
  resources: resourcesWithEnabled,
1892
1931
  createTime,
1893
1932
  enabled,
1933
+ // Resolved npx/uvx package version + registry update hint (#1166).
1934
+ // Runtime state, kept at the top level like `version` so it can never
1935
+ // be round-tripped into a persisted ServerConfig.
1936
+ ...(packageVersion ? { packageVersion } : {}),
1937
+ ...(latestVersion ? { latestVersion } : {}),
1938
+ ...(updateAvailable !== undefined ? { updateAvailable } : {}),
1894
1939
  oauth: oauth || oauthConnected
1895
1940
  ? {
1896
1941
  ...(oauth
@@ -1969,6 +2014,75 @@ export const reconnectServer = async (serverName) => {
1969
2014
  await initializeClientsFromSettings(false, serverName);
1970
2015
  logger.log(`Successfully reconnected server: ${serverName}`);
1971
2016
  };
2017
+ /**
2018
+ * Resolve the installed package version for an npx/uvx stdio server and record
2019
+ * it on the live ServerInfo, so the dashboard can answer "what version is
2020
+ * actually running" (see #1166). Also checks the configured registry for a
2021
+ * newer version and marks updateAvailable. Fire-and-forget: best-effort
2022
+ * display data — a resolution failure must never break or delay a connection.
2023
+ *
2024
+ * On any failure the runtime fields are cleared so the UI never shows a stale
2025
+ * version or a stale "update available" hint from an earlier, healthier check.
2026
+ */
2027
+ const applyResolvedPackageVersion = (info, config) => {
2028
+ const command = config?.command;
2029
+ if (!command || !supportsCacheRefresh(command))
2030
+ return;
2031
+ void (async () => {
2032
+ try {
2033
+ const args = config.args ?? [];
2034
+ const version = await resolveRunnerPackageVersion(command, args);
2035
+ if (!version) {
2036
+ info.packageVersion = undefined;
2037
+ info.latestVersion = undefined;
2038
+ info.updateAvailable = undefined;
2039
+ return;
2040
+ }
2041
+ info.packageVersion = version;
2042
+ const systemConfig = await getSystemConfigDao().get();
2043
+ const update = await checkPackageUpdate(command, args, version, {
2044
+ npmRegistry: systemConfig?.install?.npmRegistry,
2045
+ pythonIndexUrl: systemConfig?.install?.pythonIndexUrl,
2046
+ });
2047
+ if (update?.latestVersion) {
2048
+ info.latestVersion = update.latestVersion;
2049
+ info.updateAvailable = update.updateAvailable;
2050
+ }
2051
+ else {
2052
+ // Registry unreachable or no metadata: hide the hint rather than keep
2053
+ // a stale "update available" from a previous successful check.
2054
+ info.latestVersion = undefined;
2055
+ info.updateAvailable = undefined;
2056
+ }
2057
+ }
2058
+ catch (error) {
2059
+ logger.warn(`Failed to resolve package update for ${info.name}`, { error });
2060
+ info.latestVersion = undefined;
2061
+ info.updateAvailable = undefined;
2062
+ }
2063
+ })();
2064
+ };
2065
+ // Periodically refresh the "update available" flag for npx/uvx servers (#1166).
2066
+ // Each check respects the TTL cache inside checkPackageUpdate, so this only
2067
+ // re-queries packages whose last registry lookup has gone stale. The timer is
2068
+ // unref'd so it never keeps the process alive by itself.
2069
+ //
2070
+ // Note: personal-runtime servers (credential-template bindings) are not part of
2071
+ // `serverInfos` and are therefore not swept here — their flags refresh when a
2072
+ // new per-user connection is created in createPrincipalRuntime instead.
2073
+ const PACKAGE_UPDATE_SWEEP_MS = 6 * 60 * 60 * 1000;
2074
+ const packageUpdateSweepTimer = setInterval(() => {
2075
+ for (const info of serverInfos) {
2076
+ if (!info.config?.command)
2077
+ continue;
2078
+ applyResolvedPackageVersion(info, info.config);
2079
+ }
2080
+ }, PACKAGE_UPDATE_SWEEP_MS);
2081
+ packageUpdateSweepTimer.unref?.();
2082
+ /** Stop the periodic package-update sweep (called on shutdown). */
2083
+ export const stopPackageUpdateSweep = () => {
2084
+ clearInterval(packageUpdateSweepTimer);
2085
+ };
1972
2086
  // Reinstall server: clear package cache and reconnect.
1973
2087
  // For npx: deletes ~/.npm/_npx before reconnect (--ignore-existing removed in npm 7+).
1974
2088
  // For uvx: schedules --refresh flag injection on next spawn via pendingReinstalls Set.
@@ -2303,6 +2417,8 @@ const ensureServerReady = async (serverInfo) => {
2303
2417
  serverInfo.version = client.getServerVersion?.()?.version;
2304
2418
  serverInfo.instructions = client.getInstructions?.();
2305
2419
  serverInfo.config = expandedConf;
2420
+ // Record the resolved npx/uvx package version backing this server.
2421
+ applyResolvedPackageVersion(serverInfo, expandedConf);
2306
2422
  serverInfo.status = 'connected';
2307
2423
  serverInfo.error = null;
2308
2424
  let toolCount = 0;
@@ -3569,9 +3685,12 @@ const createPrincipalRuntime = async (name, resolvedConfig) => {
3569
3685
  try {
3570
3686
  return await value.apply(target, args);
3571
3687
  }
3572
- catch {
3573
- info.status = 'disconnected';
3574
- throw new CredentialBindingError(`Personal credential request failed for '${name}'. Check your binding in Credentials.`);
3688
+ catch (error) {
3689
+ if (isUpstreamConnectionFailure(error))
3690
+ info.status = 'disconnected';
3691
+ const failure = createUpstreamRequestError(name, String(property), error, resolvedConfig);
3692
+ logger.error(failure.message);
3693
+ throw failure;
3575
3694
  }
3576
3695
  };
3577
3696
  },
@@ -3606,6 +3725,8 @@ const createPrincipalRuntime = async (name, resolvedConfig) => {
3606
3725
  if (capabilities?.resources)
3607
3726
  info.resources = (await client.listResources({}, resolvedConfig.options)).resources.map(normalizeResourceForCache);
3608
3727
  info.version = client.getServerVersion()?.version;
3728
+ // Record the resolved npx/uvx package version backing this server.
3729
+ applyResolvedPackageVersion(info, resolvedConfig);
3609
3730
  client.onclose = () => {
3610
3731
  info.status = 'disconnected';
3611
3732
  };
@@ -3613,12 +3734,36 @@ const createPrincipalRuntime = async (name, resolvedConfig) => {
3613
3734
  info.status = 'connected';
3614
3735
  return info;
3615
3736
  }
3616
- catch {
3737
+ catch (error) {
3617
3738
  closeServerRuntime(info);
3618
- throw new CredentialBindingError(`Unable to connect '${name}' with your personal credentials. Check your binding in Credentials.`);
3739
+ const failure = createUpstreamRequestError(name, 'connect', error, resolvedConfig);
3740
+ logger.error(failure.message);
3741
+ throw failure;
3619
3742
  }
3620
3743
  };
3621
3744
  const principalRuntimes = new PrincipalRuntimeService(createPrincipalRuntime, closeServerRuntime);
3745
+ /**
3746
+ * Resolve the tools of a personal-credential server on behalf of an explicit
3747
+ * principal, outside any request context.
3748
+ *
3749
+ * Servers that declare a credential template are never connected globally (see
3750
+ * the `hasCredentialTemplate` branch in `initializeClientsFromSettings`); their
3751
+ * runtime only exists for the principals who bound their own credentials. Any
3752
+ * caller that needs the tool list of such a server — smart routing reindex, for
3753
+ * instance — must therefore borrow one of those principals instead of relying
3754
+ * on the ambient request user, which may hold no binding at all.
3755
+ *
3756
+ * The returned list is a snapshot: the lease is released before returning.
3757
+ */
3758
+ export const getServerToolsForPrincipal = async (serverName, principal) => {
3759
+ const lease = await principalRuntimes.acquire(serverName, principal);
3760
+ try {
3761
+ return lease.info.tools ?? [];
3762
+ }
3763
+ finally {
3764
+ lease.release();
3765
+ }
3766
+ };
3622
3767
  const withPrincipalServers = (handler, operation) => async (request, extra) => {
3623
3768
  if (!serverInfos.some((info) => hasCredentialTemplate(info.config)))
3624
3769
  return handler(request, extra);
@@ -3641,12 +3786,25 @@ const withPrincipalServers = (handler, operation) => async (request, extra) => {
3641
3786
  const requestedName = ['call_tool', 'describe_tool'].includes(request?.params?.name)
3642
3787
  ? request.params.arguments?.toolName
3643
3788
  : request?.params?.name;
3644
- const target = candidates.find((info) => {
3645
- const exposedName = getExposedServerName(info.name, serverConfigsByName.get(info.name));
3646
- return (extra?.server === info.name ||
3647
- (typeof requestedName === 'string' &&
3648
- requestedName.startsWith(`${exposedName}${getNameSeparator()}`)));
3649
- });
3789
+ // An explicit REST target takes precedence over every qualified-name prefix.
3790
+ // Otherwise prefer the longest exposed prefix, including group aliases.
3791
+ let target = extra?.server
3792
+ ? candidates.find((info) => info.name === extra.server)
3793
+ : undefined;
3794
+ if (extra?.server && !target && (operation === 'tool' || operation === 'prompt')) {
3795
+ throw new ToolUnavailableError(`Tool not available: ${requestedName}`, 'server-or-name-not-found');
3796
+ }
3797
+ if (!extra?.server && typeof requestedName === 'string') {
3798
+ let longestPrefix = 0;
3799
+ for (const info of candidates) {
3800
+ const exposedName = getExposedServerName(info.name, serverConfigsByName.get(info.name));
3801
+ const prefix = `${exposedName}${getNameSeparator()}`;
3802
+ if (prefix.length > longestPrefix && requestedName.startsWith(prefix)) {
3803
+ target = info;
3804
+ longestPrefix = prefix.length;
3805
+ }
3806
+ }
3807
+ }
3650
3808
  // A call to a known target must not spawn or keep unrelated personal children alive.
3651
3809
  const selected = target && (operation === 'tool' || operation === 'prompt') ? [target] : candidates;
3652
3810
  const templated = selected.filter((info) => hasCredentialTemplate(info.config));
@@ -3668,9 +3826,9 @@ const withPrincipalServers = (handler, operation) => async (request, extra) => {
3668
3826
  scoped.set(info.name, lease.info);
3669
3827
  }
3670
3828
  catch (error) {
3671
- failures.set(info.name, error instanceof CredentialBindingError
3829
+ failures.set(info.name, error instanceof CredentialBindingError || error instanceof UpstreamRequestError
3672
3830
  ? error
3673
- : new CredentialBindingError('Unable to resolve personal credentials'));
3831
+ : new Error(`Unable to prepare upstream '${info.name}'`));
3674
3832
  }
3675
3833
  }));
3676
3834
  const requiredTarget = target ||