@samanhappy/mcphub 1.0.16 → 1.0.18

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 (92) hide show
  1. package/dist/betterAuth.js +2 -2
  2. package/dist/betterAuth.js.map +1 -1
  3. package/dist/cli/commands/cache.js +39 -0
  4. package/dist/cli/commands/cache.js.map +1 -0
  5. package/dist/cli/commands/servers.js +14 -0
  6. package/dist/cli/commands/servers.js.map +1 -1
  7. package/dist/cli/help.js +11 -2
  8. package/dist/cli/help.js.map +1 -1
  9. package/dist/cli/main.js +1 -0
  10. package/dist/cli/main.js.map +1 -1
  11. package/dist/controllers/groupController.js +66 -2
  12. package/dist/controllers/groupController.js.map +1 -1
  13. package/dist/controllers/serverController.js +62 -1
  14. package/dist/controllers/serverController.js.map +1 -1
  15. package/dist/db/entities/Group.js.map +1 -1
  16. package/dist/middlewares/auth.js +20 -7
  17. package/dist/middlewares/auth.js.map +1 -1
  18. package/dist/routes/index.js +3 -1
  19. package/dist/routes/index.js.map +1 -1
  20. package/dist/services/groupService.js +25 -0
  21. package/dist/services/groupService.js.map +1 -1
  22. package/dist/services/mcpService.js +270 -25
  23. package/dist/services/mcpService.js.map +1 -1
  24. package/dist/services/smartRoutingService.js +63 -12
  25. package/dist/services/smartRoutingService.js.map +1 -1
  26. package/dist/utils/cacheUtils.js +110 -0
  27. package/dist/utils/cacheUtils.js.map +1 -0
  28. package/frontend/dist/assets/{ActivityPage-BYPkr9n2.js → ActivityPage-C36IBbnj.js} +2 -2
  29. package/frontend/dist/assets/{ActivityPage-BYPkr9n2.js.map → ActivityPage-C36IBbnj.js.map} +1 -1
  30. package/frontend/dist/assets/{ConfirmDialog-Cag_haxr.js → ConfirmDialog-BKNVI65J.js} +2 -2
  31. package/frontend/dist/assets/{ConfirmDialog-Cag_haxr.js.map → ConfirmDialog-BKNVI65J.js.map} +1 -1
  32. package/frontend/dist/assets/{Dashboard-BbGvQMHv.js → Dashboard-BsUNNsqb.js} +2 -2
  33. package/frontend/dist/assets/{Dashboard-BbGvQMHv.js.map → Dashboard-BsUNNsqb.js.map} +1 -1
  34. package/frontend/dist/assets/{DeleteDialog-BBfJpiiD.js → DeleteDialog-7NPk3Hro.js} +2 -2
  35. package/frontend/dist/assets/{DeleteDialog-BBfJpiiD.js.map → DeleteDialog-7NPk3Hro.js.map} +1 -1
  36. package/frontend/dist/assets/{EndpointCopy-DhPgdjmi.js → EndpointCopy-eF3xFbaZ.js} +2 -2
  37. package/frontend/dist/assets/{EndpointCopy-DhPgdjmi.js.map → EndpointCopy-eF3xFbaZ.js.map} +1 -1
  38. package/frontend/dist/assets/GroupsPage-Dp-NU5wZ.js +33 -0
  39. package/frontend/dist/assets/GroupsPage-Dp-NU5wZ.js.map +1 -0
  40. package/frontend/dist/assets/{LoginPage-2PH92kBp.js → LoginPage-9PIMlvGe.js} +2 -2
  41. package/frontend/dist/assets/{LoginPage-2PH92kBp.js.map → LoginPage-9PIMlvGe.js.map} +1 -1
  42. package/frontend/dist/assets/{LogsPage-D8ws1iFm.js → LogsPage-C8WS43Dw.js} +2 -2
  43. package/frontend/dist/assets/{LogsPage-D8ws1iFm.js.map → LogsPage-C8WS43Dw.js.map} +1 -1
  44. package/frontend/dist/assets/{MarketPage-BL9qUEMR.js → MarketPage-Cyl5VvwI.js} +2 -2
  45. package/frontend/dist/assets/{MarketPage-BL9qUEMR.js.map → MarketPage-Cyl5VvwI.js.map} +1 -1
  46. package/frontend/dist/assets/{Pagination-DBAu79mv.js → Pagination-DAcOi8eq.js} +2 -2
  47. package/frontend/dist/assets/{Pagination-DBAu79mv.js.map → Pagination-DAcOi8eq.js.map} +1 -1
  48. package/frontend/dist/assets/{PromptsPage-CtXO4diZ.js → PromptsPage-CqxKWAR9.js} +2 -2
  49. package/frontend/dist/assets/{PromptsPage-CtXO4diZ.js.map → PromptsPage-CqxKWAR9.js.map} +1 -1
  50. package/frontend/dist/assets/{ResourcesPage-GD-T8LpP.js → ResourcesPage-Dq7Grza0.js} +2 -2
  51. package/frontend/dist/assets/{ResourcesPage-GD-T8LpP.js.map → ResourcesPage-Dq7Grza0.js.map} +1 -1
  52. package/frontend/dist/assets/ServersPage-xt7QkwjZ.js +37 -0
  53. package/frontend/dist/assets/ServersPage-xt7QkwjZ.js.map +1 -0
  54. package/frontend/dist/assets/SettingsPage-Cn5Krvgb.js +12 -0
  55. package/frontend/dist/assets/SettingsPage-Cn5Krvgb.js.map +1 -0
  56. package/frontend/dist/assets/{StatusDot-Bp40buM9.js → StatusDot-DAStUyI3.js} +2 -2
  57. package/frontend/dist/assets/{StatusDot-Bp40buM9.js.map → StatusDot-DAStUyI3.js.map} +1 -1
  58. package/frontend/dist/assets/{ToggleGroup-B15lxnw6.js → ToggleGroup-CQvqGQLA.js} +2 -2
  59. package/frontend/dist/assets/{ToggleGroup-B15lxnw6.js.map → ToggleGroup-CQvqGQLA.js.map} +1 -1
  60. package/frontend/dist/assets/{UsersPage-Cv80MtZ4.js → UsersPage-Bsa2NGcJ.js} +2 -2
  61. package/frontend/dist/assets/{UsersPage-Cv80MtZ4.js.map → UsersPage-Bsa2NGcJ.js.map} +1 -1
  62. package/frontend/dist/assets/{contextCost-DldRDO4O.js → contextCost-DrQqHXcP.js} +2 -2
  63. package/frontend/dist/assets/{contextCost-DldRDO4O.js.map → contextCost-DrQqHXcP.js.map} +1 -1
  64. package/frontend/dist/assets/framework-vendor-X-WP1v0m.js +61 -0
  65. package/frontend/dist/assets/framework-vendor-X-WP1v0m.js.map +1 -0
  66. package/frontend/dist/assets/{i18n-vendor-DP1IRITP.js → i18n-vendor-BLr2MLKp.js} +2 -2
  67. package/frontend/dist/assets/{i18n-vendor-DP1IRITP.js.map → i18n-vendor-BLr2MLKp.js.map} +1 -1
  68. package/frontend/dist/assets/{icons-vendor-BTEm6PQs.js → icons-vendor-DMtsx1SI.js} +63 -58
  69. package/frontend/dist/assets/icons-vendor-DMtsx1SI.js.map +1 -0
  70. package/frontend/dist/assets/{index-D1-Bdpe1.css → index-BlTPJflb.css} +1 -1
  71. package/frontend/dist/assets/index-BsgjLwhT.js +3 -0
  72. package/frontend/dist/assets/index-BsgjLwhT.js.map +1 -0
  73. package/frontend/dist/assets/{resourceService-CN0gM37U.js → resourceService-Bg941cnv.js} +2 -2
  74. package/frontend/dist/assets/{resourceService-CN0gM37U.js.map → resourceService-Bg941cnv.js.map} +1 -1
  75. package/frontend/dist/assets/useSettingsData-BwKohDXD.js +2 -0
  76. package/frontend/dist/assets/{useSettingsData-DewLOhzu.js.map → useSettingsData-BwKohDXD.js.map} +1 -1
  77. package/frontend/dist/assets/{variableDetection-hIevXYOZ.js → variableDetection-Bp_stsiy.js} +2 -2
  78. package/frontend/dist/assets/{variableDetection-hIevXYOZ.js.map → variableDetection-Bp_stsiy.js.map} +1 -1
  79. package/frontend/dist/index.html +5 -5
  80. package/package.json +13 -12
  81. package/frontend/dist/assets/GroupsPage-BC9FlhX4.js +0 -33
  82. package/frontend/dist/assets/GroupsPage-BC9FlhX4.js.map +0 -1
  83. package/frontend/dist/assets/ServersPage-CXUBWjQO.js +0 -37
  84. package/frontend/dist/assets/ServersPage-CXUBWjQO.js.map +0 -1
  85. package/frontend/dist/assets/SettingsPage-C5tAS-rd.js +0 -12
  86. package/frontend/dist/assets/SettingsPage-C5tAS-rd.js.map +0 -1
  87. package/frontend/dist/assets/framework-vendor-DeqnZ0v6.js +0 -61
  88. package/frontend/dist/assets/framework-vendor-DeqnZ0v6.js.map +0 -1
  89. package/frontend/dist/assets/icons-vendor-BTEm6PQs.js.map +0 -1
  90. package/frontend/dist/assets/index-OwXusZPZ.js +0 -3
  91. package/frontend/dist/assets/index-OwXusZPZ.js.map +0 -1
  92. package/frontend/dist/assets/useSettingsData-DewLOhzu.js +0 -2
@@ -1,6 +1,7 @@
1
1
  import os from 'os';
2
2
  import path from 'path';
3
3
  import fs from 'fs';
4
+ import treeKill from 'tree-kill';
4
5
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
5
6
  import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
6
7
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
@@ -26,6 +27,7 @@ import { maybeCompressToolResult } from './toolResultCompressionService.js';
26
27
  import { assertHostedToolAllowed, filterHostedTools, reserveHostedToolCall, settleHostedToolCall, } from './hostedAuthService.js';
27
28
  import { formatErrorForLogging, sanitizeStringForLogging, summarizeErrorForLogging, } from '../utils/serialization.js';
28
29
  import { MCP_APPS_CAPABILITIES, filterModelVisibleTools, hasMcpAppsCapability, isAppOnlyTool, stripMcpAppsMetadata, } from '../utils/mcpApps.js';
30
+ import { supportsCacheRefresh, injectRefreshFlag, clearRunnerCache } from '../utils/cacheUtils.js';
29
31
  const servers = {};
30
32
  import { setupClientKeepAlive } from './keepAliveService.js';
31
33
  /**
@@ -321,6 +323,13 @@ const normalizeResourceForCache = (resource) => {
321
323
  };
322
324
  // Store all server information
323
325
  let serverInfos = [];
326
+ // Track servers pending a cache-refresh reinstall.
327
+ // Consumed once by createTransportFromConfig on the next reconnect.
328
+ const pendingReinstalls = new Set();
329
+ // Grace period after sending SIGTERM to a stdio process tree before falling back
330
+ // to SIGKILL. Long enough to let well-behaved servers shut down cleanly, short
331
+ // enough that a hung child does not block the container indefinitely.
332
+ const STDIO_KILL_GRACE_PERIOD_MS = 2000;
324
333
  // Test-only helper to set serverInfos directly. Not for production use.
325
334
  export const setServerInfosForTest = (infos) => {
326
335
  serverInfos = infos;
@@ -645,7 +654,9 @@ const summarizePromptForLogging = (prompt) => {
645
654
  return summary;
646
655
  };
647
656
  export const collectPassthroughHeaders = (requestHeaders, passthroughHeaderNames) => {
648
- if (!requestHeaders || !Array.isArray(passthroughHeaderNames) || passthroughHeaderNames.length === 0) {
657
+ if (!requestHeaders ||
658
+ !Array.isArray(passthroughHeaderNames) ||
659
+ passthroughHeaderNames.length === 0) {
649
660
  return {};
650
661
  }
651
662
  const passthroughHeaders = {};
@@ -754,7 +765,15 @@ export const createTransportFromConfig = async (name, conf) => {
754
765
  env['npm_config_registry'] = systemConfig.install.npmRegistry;
755
766
  }
756
767
  // Apply proxychains4 wrapper if proxy is configured (Linux/macOS only)
757
- const { command: finalCommand, args: finalArgs } = wrapWithProxychains(name, conf.command, replaceEnvVars(conf.args), conf.proxy);
768
+ let resolvedArgs = replaceEnvVars(conf.args);
769
+ // If this server is pending a reinstall, inject cache-busting flags (uvx only).
770
+ // For npx, the cache directory was already cleared before reconnect.
771
+ if (pendingReinstalls.has(name)) {
772
+ resolvedArgs = injectRefreshFlag(conf.command, resolvedArgs);
773
+ pendingReinstalls.delete(name);
774
+ console.log(`[${name}] Injected cache refresh flags for reinstall`);
775
+ }
776
+ const { command: finalCommand, args: finalArgs } = wrapWithProxychains(name, conf.command, resolvedArgs, conf.proxy);
758
777
  // Create STDIO transport with potentially wrapped command
759
778
  transport = new StdioClientTransport({
760
779
  cwd: process.cwd(),
@@ -880,9 +899,18 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
880
899
  });
881
900
  continue;
882
901
  }
883
- // Check if server is already connected
884
- const existingServer = existingServerInfos.find((s) => s.name === name && s.status === 'connected');
885
- if (existingServer && (!serverName || serverName !== name)) {
902
+ // Reuse this server's existing runtime state instead of reconnecting when:
903
+ // - a targeted reload/reconnect (serverName) was requested for a
904
+ // *different* server preserve its current state regardless of
905
+ // status. Reloading one server must not reconnect unrelated servers
906
+ // (e.g. failed/disconnected ones), which would also leak their
907
+ // previous stdio child processes. See #921.
908
+ // - a general/full initialization (no serverName) — only preserve this
909
+ // server's state if it is already connected.
910
+ const existingServer = existingServerInfos.find((s) => s.name === name);
911
+ const isDifferentServer = Boolean(serverName) && serverName !== name;
912
+ if (existingServer &&
913
+ (isDifferentServer || (!serverName && existingServer.status === 'connected'))) {
886
914
  nextServerInfos.push({
887
915
  ...existingServer,
888
916
  enabled: expandedConf.enabled === undefined ? true : expandedConf.enabled,
@@ -1230,10 +1258,14 @@ export const getServersInfo = async (page, limit, user) => {
1230
1258
  // Don't expose codeVerifier to frontend for security
1231
1259
  }
1232
1260
  : undefined,
1233
- config: resolvedType || serverConfig?.description
1261
+ config: resolvedType || serverConfig?.description || serverConfig?.command
1234
1262
  ? {
1235
1263
  ...(resolvedType ? { type: resolvedType } : {}),
1236
1264
  ...(serverConfig?.description ? { description: serverConfig.description } : {}),
1265
+ // Expose command so the frontend can determine if reinstall is
1266
+ // supported (npx/uvx only). This is not a secret — it's the
1267
+ // runner binary name (e.g. "npx", "uvx").
1268
+ ...(serverConfig?.command ? { command: serverConfig.command } : {}),
1237
1269
  }
1238
1270
  : undefined,
1239
1271
  };
@@ -1289,6 +1321,42 @@ export const reconnectServer = async (serverName) => {
1289
1321
  await initializeClientsFromSettings(false, serverName);
1290
1322
  console.log(`Successfully reconnected server: ${serverName}`);
1291
1323
  };
1324
+ // Reinstall server: clear package cache and reconnect.
1325
+ // For npx: deletes ~/.npm/_npx before reconnect (--ignore-existing removed in npm 7+).
1326
+ // For uvx: schedules --refresh flag injection on next spawn via pendingReinstalls Set.
1327
+ export const reinstallServer = async (serverName) => {
1328
+ console.log(`Reinstalling server: ${serverName}`);
1329
+ const serverInfo = getServerByName(serverName);
1330
+ if (!serverInfo) {
1331
+ throw new Error(`Server not found: ${serverName}`);
1332
+ }
1333
+ const serverConfig = await getServerDao().findById(serverName);
1334
+ if (!serverConfig) {
1335
+ throw new Error(`Server configuration not found: ${serverName}`);
1336
+ }
1337
+ if (serverConfig.enabled === false) {
1338
+ throw new Error(`Cannot reinstall a disabled server: ${serverName}`);
1339
+ }
1340
+ const command = serverConfig.command;
1341
+ if (!command || !supportsCacheRefresh(command)) {
1342
+ throw new Error(`Server "${serverName}" does not support cache refresh (command: ${command || 'none'}). Only npx and uvx servers are supported.`);
1343
+ }
1344
+ // Mark server as pending reinstall (consumed by createTransportFromConfig for uvx)
1345
+ pendingReinstalls.add(serverName);
1346
+ try {
1347
+ // For npx, clear cache directory synchronously before reconnect.
1348
+ // For uvx, this is a no-op — refresh is handled via --refresh flag injection.
1349
+ await clearRunnerCache(command);
1350
+ // Close and reconnect (will pick up pendingReinstalls flag for uvx)
1351
+ await reconnectServer(serverName);
1352
+ console.log(`Successfully initiated reinstall for server: ${serverName}`);
1353
+ }
1354
+ catch (error) {
1355
+ // Clean up pendingReinstalls on failure to avoid stale entries
1356
+ pendingReinstalls.delete(serverName);
1357
+ throw error;
1358
+ }
1359
+ };
1292
1360
  // Filter tools by server configuration
1293
1361
  const filterToolsByConfig = async (serverName, tools) => {
1294
1362
  const serverConfig = await getServerDao().findById(serverName);
@@ -1323,6 +1391,11 @@ export const removeServer = async (name) => {
1323
1391
  if (!result) {
1324
1392
  return { success: false, message: 'Failed to remove server' };
1325
1393
  }
1394
+ // Close the client and terminate the underlying child process tree BEFORE
1395
+ // dropping the serverInfos reference. Without this, a stdio child launched
1396
+ // via npx / npm exec outlives the request and becomes an unkillable orphan
1397
+ // that leaks memory until the container is restarted.
1398
+ closeServer(name);
1326
1399
  try {
1327
1400
  await removeServerToolEmbeddings(name);
1328
1401
  }
@@ -1373,7 +1446,10 @@ function checkAuthError(result) {
1373
1446
  // Ignore JSON parse errors and continue
1374
1447
  return;
1375
1448
  }
1376
- if (errorContent.code === 401) {
1449
+ // JSON.parse can yield null or a primitive (e.g. text "null", "42",
1450
+ // "true") — only an object payload can carry an auth error code, so guard
1451
+ // the property access to avoid crashing on non-object results.
1452
+ if (errorContent && typeof errorContent === 'object' && errorContent.code === 401) {
1377
1453
  throw new Error('Error POSTing to endpoint (HTTP 401 Unauthorized)');
1378
1454
  }
1379
1455
  }
@@ -1389,10 +1465,76 @@ function closeServer(name) {
1389
1465
  serverInfo.keepAliveIntervalId = undefined;
1390
1466
  console.log(`Cleared keep-alive interval for server: ${serverInfo.name}`);
1391
1467
  }
1468
+ // Capture the child PID via duck-typing. `instanceof StdioClientTransport`
1469
+ // is unreliable under pnpm's "dual package hazard" — a different copy of
1470
+ // @modelcontextprotocol/sdk in node_modules makes the check return false
1471
+ // even for genuine stdio transports. The `pid` getter is the SDK's public
1472
+ // contract, so checking for it is both safer and version-agnostic.
1473
+ const candidateTransport = serverInfo.transport;
1474
+ const stdioPid = typeof candidateTransport.pid === 'number' ? candidateTransport.pid : null;
1392
1475
  serverInfo.client.close();
1393
1476
  serverInfo.transport.close();
1477
+ if (stdioPid) {
1478
+ killStdioProcessTree(name, stdioPid);
1479
+ }
1394
1480
  console.log(`Closed client and transport for server: ${serverInfo.name}`);
1395
- // TODO kill process
1481
+ }
1482
+ }
1483
+ // Kill the entire process tree of a stdio transport's child process.
1484
+ //
1485
+ // transport.close() only sends SIGTERM to the direct child. When the server is
1486
+ // launched through a wrapper like `npx` / `npm exec`, the wrapper does not
1487
+ // forward signals to its descendants, so the real server process is left
1488
+ // running as an orphan. Walk the whole tree and force-kill it.
1489
+ function killStdioProcessTree(name, pid) {
1490
+ const safeTreeKill = (signal) => {
1491
+ try {
1492
+ treeKill(pid, signal, (err) => {
1493
+ if (err) {
1494
+ // ESRCH (no such process) is expected when the process already exited
1495
+ // — treat as success. Anything else is worth a warning.
1496
+ const code = err.code;
1497
+ if (code !== 'ESRCH') {
1498
+ // Pass the user-controlled `name` as a separate argument so a
1499
+ // server named e.g. "%s" cannot inject format specifiers into the
1500
+ // log line (CodeQL: use-of-externally-controlled-format-string).
1501
+ console.warn('Failed to send signal to process tree', {
1502
+ serverName: name,
1503
+ pid,
1504
+ signal,
1505
+ err,
1506
+ });
1507
+ }
1508
+ }
1509
+ });
1510
+ }
1511
+ catch (err) {
1512
+ console.warn('Failed to send signal to process tree', {
1513
+ serverName: name,
1514
+ pid,
1515
+ signal,
1516
+ err,
1517
+ });
1518
+ }
1519
+ };
1520
+ safeTreeKill('SIGTERM');
1521
+ setTimeout(() => {
1522
+ if (!isProcessAlive(pid)) {
1523
+ return;
1524
+ }
1525
+ safeTreeKill('SIGKILL');
1526
+ }, STDIO_KILL_GRACE_PERIOD_MS);
1527
+ }
1528
+ function isProcessAlive(pid) {
1529
+ try {
1530
+ process.kill(pid, 0);
1531
+ return true;
1532
+ }
1533
+ catch (err) {
1534
+ // EPERM means the process exists but we don't have permission to signal
1535
+ // it — count it as alive so the SIGKILL fallback still fires. Any other
1536
+ // error (typically ESRCH) means the process is gone.
1537
+ return err.code === 'EPERM';
1396
1538
  }
1397
1539
  }
1398
1540
  // Toggle server enabled status
@@ -1464,6 +1606,31 @@ const normalizeToolNameForServer = (serverName, toolName) => {
1464
1606
  const prefix = `${serverName}${getNameSeparator()}`;
1465
1607
  return toolName.startsWith(prefix) ? toolName.substring(prefix.length) : toolName;
1466
1608
  };
1609
+ const getGroupLookupName = (group) => {
1610
+ if (group?.startsWith('$smart/')) {
1611
+ return group.substring(7) || undefined;
1612
+ }
1613
+ return group;
1614
+ };
1615
+ const getExposedServerName = (serverName, serverConfig) => {
1616
+ return serverConfig?.alias?.trim() || serverName;
1617
+ };
1618
+ const replacePrefixedServerName = (name, fromServerName, toServerName) => {
1619
+ if (fromServerName === toServerName) {
1620
+ return name;
1621
+ }
1622
+ const separator = getNameSeparator();
1623
+ const prefix = `${fromServerName}${separator}`;
1624
+ return name.startsWith(prefix)
1625
+ ? `${toServerName}${separator}${name.substring(prefix.length)}`
1626
+ : name;
1627
+ };
1628
+ const projectNameForGroup = (name, serverName, serverConfig) => {
1629
+ return replacePrefixedServerName(name, serverName, getExposedServerName(serverName, serverConfig));
1630
+ };
1631
+ const resolveNameFromGroup = (name, serverName, serverConfig) => {
1632
+ return replacePrefixedServerName(name, getExposedServerName(serverName, serverConfig), serverName);
1633
+ };
1467
1634
  const findToolOnServer = (serverInfo, toolName, allowRawName) => {
1468
1635
  return serverInfo.tools.find((tool) => tool.name === toolName ||
1469
1636
  (allowRawName && normalizeToolNameForServer(serverInfo.name, tool.name) === toolName));
@@ -1473,7 +1640,55 @@ const assertToolAvailableForRoute = (tool, appsRouteContext) => {
1473
1640
  throw new Error(`Tool '${tool.name}' is only available to MCP Apps`);
1474
1641
  }
1475
1642
  };
1476
- const projectToolForDownstream = (serverName, tool, appsRouteContext) => {
1643
+ const resolveToolInGroup = async (group, toolName, allowRawName) => {
1644
+ const lookupGroup = getGroupLookupName(group);
1645
+ if (!lookupGroup) {
1646
+ return undefined;
1647
+ }
1648
+ const { filteredServerInfos, serverConfigsByName } = await getFilteredServerInfosForGroup(lookupGroup);
1649
+ for (const serverInfo of filteredServerInfos) {
1650
+ if (serverInfo.status !== 'connected' || serverInfo.enabled === false) {
1651
+ continue;
1652
+ }
1653
+ const serverConfig = serverConfigsByName.get(serverInfo.name);
1654
+ const internalToolName = resolveNameFromGroup(toolName, serverInfo.name, serverConfig);
1655
+ const tool = findToolOnServer(serverInfo, internalToolName, allowRawName);
1656
+ if (!tool) {
1657
+ continue;
1658
+ }
1659
+ const filteredTools = await filterToolsByGroup(lookupGroup, serverInfo.name, [tool], serverConfig);
1660
+ if (filteredTools.length === 0) {
1661
+ continue;
1662
+ }
1663
+ return { serverInfo, toolName: internalToolName, tool };
1664
+ }
1665
+ return undefined;
1666
+ };
1667
+ async function resolvePromptInGroup(group, promptName) {
1668
+ const lookupGroup = getGroupLookupName(group);
1669
+ if (!lookupGroup) {
1670
+ return undefined;
1671
+ }
1672
+ const { filteredServerInfos, serverConfigsByName } = await getFilteredServerInfosForGroup(lookupGroup);
1673
+ for (const serverInfo of filteredServerInfos) {
1674
+ if (serverInfo.status !== 'connected' || serverInfo.enabled === false) {
1675
+ continue;
1676
+ }
1677
+ const serverConfig = serverConfigsByName.get(serverInfo.name);
1678
+ const internalPromptName = resolveNameFromGroup(promptName, serverInfo.name, serverConfig);
1679
+ const prompt = serverInfo.prompts.find((item) => item.name === internalPromptName);
1680
+ if (!prompt) {
1681
+ continue;
1682
+ }
1683
+ const filteredPrompts = await filterPromptsByGroup(lookupGroup, serverInfo.name, [prompt], serverConfig);
1684
+ if (filteredPrompts.length === 0) {
1685
+ continue;
1686
+ }
1687
+ return { serverInfo, promptName: internalPromptName };
1688
+ }
1689
+ return undefined;
1690
+ }
1691
+ const projectToolForDownstream = (serverName, tool, appsRouteContext, serverConfig) => {
1477
1692
  if (!appsRouteContext.enabled && isAppOnlyTool(tool)) {
1478
1693
  return undefined;
1479
1694
  }
@@ -1482,7 +1697,7 @@ const projectToolForDownstream = (serverName, tool, appsRouteContext) => {
1482
1697
  ...projectedTool,
1483
1698
  name: appsRouteContext.enabled
1484
1699
  ? normalizeToolNameForServer(serverName, projectedTool.name)
1485
- : projectedTool.name,
1700
+ : projectNameForGroup(projectedTool.name, serverName, serverConfig),
1486
1701
  };
1487
1702
  };
1488
1703
  export const handleListToolsRequest = async (_, extra) => {
@@ -1499,10 +1714,11 @@ export const handleListToolsRequest = async (_, extra) => {
1499
1714
  const allTools = [];
1500
1715
  for (const serverInfo of filteredServerInfos) {
1501
1716
  if (serverInfo.tools && serverInfo.tools.length > 0) {
1717
+ const groupServerConfig = serverConfigsByName.get(serverInfo.name);
1502
1718
  // Filter tools based on server configuration
1503
1719
  let tools = await filterToolsByConfig(serverInfo.name, serverInfo.tools);
1504
1720
  // If this is a group request, apply group-level tool filtering
1505
- tools = await filterToolsByGroup(group, serverInfo.name, tools, serverConfigsByName.get(serverInfo.name));
1721
+ tools = await filterToolsByGroup(group, serverInfo.name, tools, groupServerConfig);
1506
1722
  // Apply custom descriptions from server configuration
1507
1723
  const serverConfig = await getServerDao().findById(serverInfo.name);
1508
1724
  const toolsWithCustomDescriptions = tools.map((tool) => {
@@ -1513,7 +1729,7 @@ export const handleListToolsRequest = async (_, extra) => {
1513
1729
  };
1514
1730
  });
1515
1731
  allTools.push(...toolsWithCustomDescriptions.flatMap((tool) => {
1516
- const projectedTool = projectToolForDownstream(serverInfo.name, tool, appsRouteContext);
1732
+ const projectedTool = projectToolForDownstream(serverInfo.name, tool, appsRouteContext, groupServerConfig);
1517
1733
  return projectedTool ? [projectedTool] : [];
1518
1734
  }));
1519
1735
  }
@@ -1574,12 +1790,22 @@ export const handleCallToolRequest = async (request, extra) => {
1574
1790
  }
1575
1791
  const { arguments: toolArgs } = request.params.arguments || {};
1576
1792
  let targetServerInfo;
1793
+ let targetToolName = toolName;
1794
+ let targetTool;
1577
1795
  if (appsRouteContext.enabled) {
1578
1796
  targetServerInfo = appsRouteContext.serverInfo;
1579
1797
  }
1580
1798
  else if (extra && extra.server) {
1581
1799
  targetServerInfo = getServerByName(extra.server);
1582
1800
  }
1801
+ else if (group) {
1802
+ const groupTool = await resolveToolInGroup(group, toolName, appsRouteContext.enabled);
1803
+ if (groupTool) {
1804
+ targetServerInfo = groupTool.serverInfo;
1805
+ targetToolName = groupTool.toolName;
1806
+ targetTool = groupTool.tool;
1807
+ }
1808
+ }
1583
1809
  else {
1584
1810
  // Find the first server that has this tool
1585
1811
  targetServerInfo = serverInfos.find((serverInfo) => serverInfo.status === 'connected' &&
@@ -1590,7 +1816,7 @@ export const handleCallToolRequest = async (request, extra) => {
1590
1816
  throw new Error(`No available servers found with tool: ${toolName}`);
1591
1817
  }
1592
1818
  // Check if the tool exists on the server
1593
- const tool = findToolOnServer(targetServerInfo, toolName, appsRouteContext.enabled);
1819
+ const tool = targetTool ?? findToolOnServer(targetServerInfo, targetToolName, appsRouteContext.enabled);
1594
1820
  if (!tool) {
1595
1821
  throw new Error(`Tool '${toolName}' not found on server '${targetServerInfo.name}'`);
1596
1822
  }
@@ -1602,12 +1828,12 @@ export const handleCallToolRequest = async (request, extra) => {
1602
1828
  // Use toolArgs if it has properties, otherwise fallback to request.params.arguments
1603
1829
  const finalArgs = toolArgs && typeof toolArgs === 'object' ? toolArgs : {};
1604
1830
  console.log('Invoking OpenAPI tool', {
1605
- toolName,
1831
+ toolName: targetToolName,
1606
1832
  serverName: targetServerInfo.name,
1607
1833
  arguments: summarizeArgumentsForLogging(finalArgs),
1608
1834
  });
1609
1835
  // Remove server prefix from tool name if present
1610
- const cleanToolName = normalizeToolNameForServer(targetServerInfo.name, toolName);
1836
+ const cleanToolName = normalizeToolNameForServer(targetServerInfo.name, targetToolName);
1611
1837
  // Extract passthrough headers from extra or request context
1612
1838
  let passthroughHeaders;
1613
1839
  let requestHeaders = null;
@@ -1680,11 +1906,11 @@ export const handleCallToolRequest = async (request, extra) => {
1680
1906
  // Use toolArgs if it has properties, otherwise fallback to request.params.arguments
1681
1907
  const finalArgs = toolArgs && typeof toolArgs === 'object' ? toolArgs : {};
1682
1908
  console.log('Invoking tool', {
1683
- toolName,
1909
+ toolName: targetToolName,
1684
1910
  serverName: targetServerInfo.name,
1685
1911
  arguments: summarizeArgumentsForLogging(finalArgs),
1686
1912
  });
1687
- const cleanToolName = normalizeToolNameForServer(targetServerInfo.name, toolName);
1913
+ const cleanToolName = normalizeToolNameForServer(targetServerInfo.name, targetToolName);
1688
1914
  await reserveHostedIfNeeded(targetServerInfo.name, cleanToolName);
1689
1915
  const result = await callToolWithReconnect(targetServerInfo, {
1690
1916
  name: cleanToolName,
@@ -1723,12 +1949,17 @@ export const handleCallToolRequest = async (request, extra) => {
1723
1949
  });
1724
1950
  }
1725
1951
  // Regular tool handling
1952
+ const groupTool = !appsRouteContext.enabled && group
1953
+ ? await resolveToolInGroup(group, request.params.name, appsRouteContext.enabled)
1954
+ : undefined;
1726
1955
  const serverInfo = appsRouteContext.enabled
1727
1956
  ? appsRouteContext.serverInfo
1728
- : getServerByTool(request.params.name);
1729
- const tool = serverInfo
1730
- ? findToolOnServer(serverInfo, request.params.name, appsRouteContext.enabled)
1731
- : undefined;
1957
+ : (groupTool?.serverInfo ?? (group ? undefined : getServerByTool(request.params.name)));
1958
+ const routeToolName = groupTool?.toolName ?? request.params.name;
1959
+ const tool = groupTool?.tool ??
1960
+ (serverInfo
1961
+ ? findToolOnServer(serverInfo, routeToolName, appsRouteContext.enabled)
1962
+ : undefined);
1732
1963
  if (!serverInfo || !tool) {
1733
1964
  throw new Error(`Server not found: ${request.params.name}`);
1734
1965
  }
@@ -1738,7 +1969,7 @@ export const handleCallToolRequest = async (request, extra) => {
1738
1969
  // For OpenAPI servers, use the OpenAPI client
1739
1970
  const openApiClient = serverInfo.openApiClient;
1740
1971
  // Remove server prefix from tool name if present
1741
- const cleanToolName = normalizeToolNameForServer(serverInfo.name, request.params.name);
1972
+ const cleanToolName = normalizeToolNameForServer(serverInfo.name, routeToolName);
1742
1973
  console.log('Invoking OpenAPI tool', {
1743
1974
  toolName: cleanToolName,
1744
1975
  serverName: serverInfo.name,
@@ -1814,7 +2045,7 @@ export const handleCallToolRequest = async (request, extra) => {
1814
2045
  if (!client) {
1815
2046
  throw new Error(`Client not found for server: ${serverInfo.name}`);
1816
2047
  }
1817
- const cleanToolName = normalizeToolNameForServer(serverInfo.name, request.params.name);
2048
+ const cleanToolName = normalizeToolNameForServer(serverInfo.name, routeToolName);
1818
2049
  await reserveHostedIfNeeded(serverInfo.name, cleanToolName);
1819
2050
  const result = await callToolWithReconnect(serverInfo, { ...request.params, name: cleanToolName }, serverInfo.options || {});
1820
2051
  await settleHostedIfNeeded({
@@ -1890,6 +2121,8 @@ export const handleCallToolRequest = async (request, extra) => {
1890
2121
  export const handleGetPromptRequest = async (request, extra) => {
1891
2122
  try {
1892
2123
  const { name, arguments: promptArgs } = request.params;
2124
+ const sessionId = extra?.sessionId || '';
2125
+ const group = extra?.group || getGroup(sessionId) || undefined;
1893
2126
  // Check built-in prompts first
1894
2127
  const builtinPrompt = await getBuiltinPromptDao().findByName(name);
1895
2128
  if (builtinPrompt && builtinPrompt.enabled !== false) {
@@ -1910,9 +2143,17 @@ export const handleGetPromptRequest = async (request, extra) => {
1910
2143
  };
1911
2144
  }
1912
2145
  let server;
2146
+ let promptNameForServer = name;
1913
2147
  if (extra && extra.server) {
1914
2148
  server = getServerByName(extra.server);
1915
2149
  }
2150
+ else if (group) {
2151
+ const groupPrompt = await resolvePromptInGroup(group, name);
2152
+ if (groupPrompt) {
2153
+ server = groupPrompt.serverInfo;
2154
+ promptNameForServer = groupPrompt.promptName;
2155
+ }
2156
+ }
1916
2157
  else {
1917
2158
  // Find the first server that has this prompt
1918
2159
  server = serverInfos.find((serverInfo) => serverInfo.status === 'connected' &&
@@ -1925,7 +2166,9 @@ export const handleGetPromptRequest = async (request, extra) => {
1925
2166
  // Remove server prefix from prompt name if present
1926
2167
  const separator = getNameSeparator();
1927
2168
  const prefix = `${server.name}${separator}`;
1928
- const cleanPromptName = name.startsWith(prefix) ? name.substring(prefix.length) : name;
2169
+ const cleanPromptName = promptNameForServer.startsWith(prefix)
2170
+ ? promptNameForServer.substring(prefix.length)
2171
+ : promptNameForServer;
1929
2172
  const promptParams = {
1930
2173
  name: cleanPromptName || '',
1931
2174
  arguments: promptArgs,
@@ -1971,6 +2214,7 @@ export const handleListPromptsRequest = async (_, extra) => {
1971
2214
  const { filteredServerInfos, serverConfigsByName } = await getFilteredServerInfosForGroup(group);
1972
2215
  for (const serverInfo of filteredServerInfos) {
1973
2216
  if (serverInfo.prompts && serverInfo.prompts.length > 0) {
2217
+ const groupServerConfig = serverConfigsByName.get(serverInfo.name);
1974
2218
  // Filter prompts based on server configuration
1975
2219
  const serverConfig = await getServerDao().findById(serverInfo.name);
1976
2220
  let enabledPrompts = serverInfo.prompts;
@@ -1981,12 +2225,13 @@ export const handleListPromptsRequest = async (_, extra) => {
1981
2225
  return promptConfig?.enabled !== false;
1982
2226
  });
1983
2227
  }
1984
- enabledPrompts = await filterPromptsByGroup(group, serverInfo.name, enabledPrompts, serverConfigsByName.get(serverInfo.name));
2228
+ enabledPrompts = await filterPromptsByGroup(group, serverInfo.name, enabledPrompts, groupServerConfig);
1985
2229
  // Apply custom descriptions from server configuration
1986
2230
  const promptsWithCustomDescriptions = enabledPrompts.map((prompt) => {
1987
2231
  const promptConfig = serverConfig?.prompts?.[prompt.name];
1988
2232
  return normalizePromptForList({
1989
2233
  ...prompt,
2234
+ name: projectNameForGroup(prompt.name, serverInfo.name, groupServerConfig),
1990
2235
  description: promptConfig?.description || prompt.description, // Use custom description if available
1991
2236
  });
1992
2237
  });