@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.
- package/dist/betterAuth.js +2 -2
- package/dist/betterAuth.js.map +1 -1
- package/dist/cli/commands/cache.js +39 -0
- package/dist/cli/commands/cache.js.map +1 -0
- package/dist/cli/commands/servers.js +14 -0
- package/dist/cli/commands/servers.js.map +1 -1
- package/dist/cli/help.js +11 -2
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +1 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/controllers/groupController.js +66 -2
- package/dist/controllers/groupController.js.map +1 -1
- package/dist/controllers/serverController.js +62 -1
- package/dist/controllers/serverController.js.map +1 -1
- package/dist/db/entities/Group.js.map +1 -1
- package/dist/middlewares/auth.js +20 -7
- package/dist/middlewares/auth.js.map +1 -1
- package/dist/routes/index.js +3 -1
- package/dist/routes/index.js.map +1 -1
- package/dist/services/groupService.js +25 -0
- package/dist/services/groupService.js.map +1 -1
- package/dist/services/mcpService.js +270 -25
- package/dist/services/mcpService.js.map +1 -1
- package/dist/services/smartRoutingService.js +63 -12
- package/dist/services/smartRoutingService.js.map +1 -1
- package/dist/utils/cacheUtils.js +110 -0
- package/dist/utils/cacheUtils.js.map +1 -0
- package/frontend/dist/assets/{ActivityPage-BYPkr9n2.js → ActivityPage-C36IBbnj.js} +2 -2
- package/frontend/dist/assets/{ActivityPage-BYPkr9n2.js.map → ActivityPage-C36IBbnj.js.map} +1 -1
- package/frontend/dist/assets/{ConfirmDialog-Cag_haxr.js → ConfirmDialog-BKNVI65J.js} +2 -2
- package/frontend/dist/assets/{ConfirmDialog-Cag_haxr.js.map → ConfirmDialog-BKNVI65J.js.map} +1 -1
- package/frontend/dist/assets/{Dashboard-BbGvQMHv.js → Dashboard-BsUNNsqb.js} +2 -2
- package/frontend/dist/assets/{Dashboard-BbGvQMHv.js.map → Dashboard-BsUNNsqb.js.map} +1 -1
- package/frontend/dist/assets/{DeleteDialog-BBfJpiiD.js → DeleteDialog-7NPk3Hro.js} +2 -2
- package/frontend/dist/assets/{DeleteDialog-BBfJpiiD.js.map → DeleteDialog-7NPk3Hro.js.map} +1 -1
- package/frontend/dist/assets/{EndpointCopy-DhPgdjmi.js → EndpointCopy-eF3xFbaZ.js} +2 -2
- package/frontend/dist/assets/{EndpointCopy-DhPgdjmi.js.map → EndpointCopy-eF3xFbaZ.js.map} +1 -1
- package/frontend/dist/assets/GroupsPage-Dp-NU5wZ.js +33 -0
- package/frontend/dist/assets/GroupsPage-Dp-NU5wZ.js.map +1 -0
- package/frontend/dist/assets/{LoginPage-2PH92kBp.js → LoginPage-9PIMlvGe.js} +2 -2
- package/frontend/dist/assets/{LoginPage-2PH92kBp.js.map → LoginPage-9PIMlvGe.js.map} +1 -1
- package/frontend/dist/assets/{LogsPage-D8ws1iFm.js → LogsPage-C8WS43Dw.js} +2 -2
- package/frontend/dist/assets/{LogsPage-D8ws1iFm.js.map → LogsPage-C8WS43Dw.js.map} +1 -1
- package/frontend/dist/assets/{MarketPage-BL9qUEMR.js → MarketPage-Cyl5VvwI.js} +2 -2
- package/frontend/dist/assets/{MarketPage-BL9qUEMR.js.map → MarketPage-Cyl5VvwI.js.map} +1 -1
- package/frontend/dist/assets/{Pagination-DBAu79mv.js → Pagination-DAcOi8eq.js} +2 -2
- package/frontend/dist/assets/{Pagination-DBAu79mv.js.map → Pagination-DAcOi8eq.js.map} +1 -1
- package/frontend/dist/assets/{PromptsPage-CtXO4diZ.js → PromptsPage-CqxKWAR9.js} +2 -2
- package/frontend/dist/assets/{PromptsPage-CtXO4diZ.js.map → PromptsPage-CqxKWAR9.js.map} +1 -1
- package/frontend/dist/assets/{ResourcesPage-GD-T8LpP.js → ResourcesPage-Dq7Grza0.js} +2 -2
- package/frontend/dist/assets/{ResourcesPage-GD-T8LpP.js.map → ResourcesPage-Dq7Grza0.js.map} +1 -1
- package/frontend/dist/assets/ServersPage-xt7QkwjZ.js +37 -0
- package/frontend/dist/assets/ServersPage-xt7QkwjZ.js.map +1 -0
- package/frontend/dist/assets/SettingsPage-Cn5Krvgb.js +12 -0
- package/frontend/dist/assets/SettingsPage-Cn5Krvgb.js.map +1 -0
- package/frontend/dist/assets/{StatusDot-Bp40buM9.js → StatusDot-DAStUyI3.js} +2 -2
- package/frontend/dist/assets/{StatusDot-Bp40buM9.js.map → StatusDot-DAStUyI3.js.map} +1 -1
- package/frontend/dist/assets/{ToggleGroup-B15lxnw6.js → ToggleGroup-CQvqGQLA.js} +2 -2
- package/frontend/dist/assets/{ToggleGroup-B15lxnw6.js.map → ToggleGroup-CQvqGQLA.js.map} +1 -1
- package/frontend/dist/assets/{UsersPage-Cv80MtZ4.js → UsersPage-Bsa2NGcJ.js} +2 -2
- package/frontend/dist/assets/{UsersPage-Cv80MtZ4.js.map → UsersPage-Bsa2NGcJ.js.map} +1 -1
- package/frontend/dist/assets/{contextCost-DldRDO4O.js → contextCost-DrQqHXcP.js} +2 -2
- package/frontend/dist/assets/{contextCost-DldRDO4O.js.map → contextCost-DrQqHXcP.js.map} +1 -1
- package/frontend/dist/assets/framework-vendor-X-WP1v0m.js +61 -0
- package/frontend/dist/assets/framework-vendor-X-WP1v0m.js.map +1 -0
- package/frontend/dist/assets/{i18n-vendor-DP1IRITP.js → i18n-vendor-BLr2MLKp.js} +2 -2
- package/frontend/dist/assets/{i18n-vendor-DP1IRITP.js.map → i18n-vendor-BLr2MLKp.js.map} +1 -1
- package/frontend/dist/assets/{icons-vendor-BTEm6PQs.js → icons-vendor-DMtsx1SI.js} +63 -58
- package/frontend/dist/assets/icons-vendor-DMtsx1SI.js.map +1 -0
- package/frontend/dist/assets/{index-D1-Bdpe1.css → index-BlTPJflb.css} +1 -1
- package/frontend/dist/assets/index-BsgjLwhT.js +3 -0
- package/frontend/dist/assets/index-BsgjLwhT.js.map +1 -0
- package/frontend/dist/assets/{resourceService-CN0gM37U.js → resourceService-Bg941cnv.js} +2 -2
- package/frontend/dist/assets/{resourceService-CN0gM37U.js.map → resourceService-Bg941cnv.js.map} +1 -1
- package/frontend/dist/assets/useSettingsData-BwKohDXD.js +2 -0
- package/frontend/dist/assets/{useSettingsData-DewLOhzu.js.map → useSettingsData-BwKohDXD.js.map} +1 -1
- package/frontend/dist/assets/{variableDetection-hIevXYOZ.js → variableDetection-Bp_stsiy.js} +2 -2
- package/frontend/dist/assets/{variableDetection-hIevXYOZ.js.map → variableDetection-Bp_stsiy.js.map} +1 -1
- package/frontend/dist/index.html +5 -5
- package/package.json +13 -12
- package/frontend/dist/assets/GroupsPage-BC9FlhX4.js +0 -33
- package/frontend/dist/assets/GroupsPage-BC9FlhX4.js.map +0 -1
- package/frontend/dist/assets/ServersPage-CXUBWjQO.js +0 -37
- package/frontend/dist/assets/ServersPage-CXUBWjQO.js.map +0 -1
- package/frontend/dist/assets/SettingsPage-C5tAS-rd.js +0 -12
- package/frontend/dist/assets/SettingsPage-C5tAS-rd.js.map +0 -1
- package/frontend/dist/assets/framework-vendor-DeqnZ0v6.js +0 -61
- package/frontend/dist/assets/framework-vendor-DeqnZ0v6.js.map +0 -1
- package/frontend/dist/assets/icons-vendor-BTEm6PQs.js.map +0 -1
- package/frontend/dist/assets/index-OwXusZPZ.js +0 -3
- package/frontend/dist/assets/index-OwXusZPZ.js.map +0 -1
- 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 ||
|
|
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
|
-
|
|
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
|
-
//
|
|
884
|
-
|
|
885
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
1730
|
-
|
|
1731
|
-
|
|
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,
|
|
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,
|
|
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 =
|
|
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,
|
|
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
|
});
|