@samanhappy/mcphub 1.1.7 → 1.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/clients/openapi.js +2 -1
- package/dist/clients/openapi.js.map +1 -1
- package/dist/controllers/authController.js +27 -1
- package/dist/controllers/authController.js.map +1 -1
- package/dist/controllers/configController.js +4 -1
- package/dist/controllers/configController.js.map +1 -1
- package/dist/controllers/oauthServerController.js +12 -14
- package/dist/controllers/oauthServerController.js.map +1 -1
- package/dist/controllers/promptController.js +2 -2
- package/dist/controllers/promptController.js.map +1 -1
- package/dist/controllers/serverController.js +201 -12
- package/dist/controllers/serverController.js.map +1 -1
- package/dist/dao/OAuthTokenDaoDbImpl.js +2 -0
- package/dist/dao/OAuthTokenDaoDbImpl.js.map +1 -1
- package/dist/dao/ServerDao.js.map +1 -1
- package/dist/dao/ServerDaoDbImpl.js.map +1 -1
- package/dist/db/entities/OAuthToken.js +4 -0
- package/dist/db/entities/OAuthToken.js.map +1 -1
- package/dist/db/entities/Server.js.map +1 -1
- package/dist/middlewares/auth.js +7 -25
- package/dist/middlewares/auth.js.map +1 -1
- package/dist/middlewares/userContext.js +6 -34
- package/dist/middlewares/userContext.js.map +1 -1
- package/dist/models/OAuth.js +14 -93
- package/dist/models/OAuth.js.map +1 -1
- package/dist/routes/index.js +3 -1
- package/dist/routes/index.js.map +1 -1
- package/dist/services/betterAuthConfig.js +1 -0
- package/dist/services/betterAuthConfig.js.map +1 -1
- package/dist/services/contextCostService.js +3 -1
- package/dist/services/contextCostService.js.map +1 -1
- package/dist/services/credentialBindingService.js +8 -3
- package/dist/services/credentialBindingService.js.map +1 -1
- package/dist/services/elicitationService.js +93 -0
- package/dist/services/elicitationService.js.map +1 -0
- package/dist/services/mcpService.js +167 -95
- package/dist/services/mcpService.js.map +1 -1
- package/dist/services/oauthServerService.js +15 -1
- package/dist/services/oauthServerService.js.map +1 -1
- package/dist/services/openApiGeneratorService.js +3 -1
- package/dist/services/openApiGeneratorService.js.map +1 -1
- package/dist/services/personalOAuthService.js +70 -8
- package/dist/services/personalOAuthService.js.map +1 -1
- package/dist/services/registrationService.js +28 -0
- package/dist/services/registrationService.js.map +1 -0
- package/dist/services/serverConfigPresenter.js +6 -1
- package/dist/services/serverConfigPresenter.js.map +1 -1
- package/dist/services/smartRoutingService.js +7 -6
- package/dist/services/smartRoutingService.js.map +1 -1
- package/dist/services/sseService.js +1 -2
- package/dist/services/sseService.js.map +1 -1
- package/dist/services/templateService.js +5 -4
- package/dist/services/templateService.js.map +1 -1
- package/dist/services/userService.js +22 -4
- package/dist/services/userService.js.map +1 -1
- package/dist/utils/credentialTemplate.js +76 -6
- package/dist/utils/credentialTemplate.js.map +1 -1
- package/dist/utils/migration.js +1 -0
- package/dist/utils/migration.js.map +1 -1
- package/dist/utils/namePattern.js +28 -0
- package/dist/utils/namePattern.js.map +1 -0
- package/dist/utils/oauthBearer.js +4 -2
- package/dist/utils/oauthBearer.js.map +1 -1
- package/dist/utils/passthroughHeaders.js +17 -0
- package/dist/utils/passthroughHeaders.js.map +1 -0
- package/dist/utils/rateLimit.js +1 -1
- package/dist/utils/rateLimit.js.map +1 -1
- package/dist/utils/serverConfigPersistence.js +1 -3
- package/dist/utils/serverConfigPersistence.js.map +1 -1
- package/dist/utils/serverConfigValidation.js +2 -1
- package/dist/utils/serverConfigValidation.js.map +1 -1
- package/dist/utils/settingsSync.js +5 -1
- package/dist/utils/settingsSync.js.map +1 -1
- package/dist/utils/toolConfig.js +93 -0
- package/dist/utils/toolConfig.js.map +1 -0
- package/dist/utils/upstreamError.js +9 -2
- package/dist/utils/upstreamError.js.map +1 -1
- package/dist/utils/userSession.js +29 -0
- package/dist/utils/userSession.js.map +1 -0
- package/frontend/dist/assets/{ActivityPage-BEpHoGLB.js → ActivityPage-CFRX20v4.js} +2 -2
- package/frontend/dist/assets/{ActivityPage-BEpHoGLB.js.map → ActivityPage-CFRX20v4.js.map} +1 -1
- package/frontend/dist/assets/{CopyClientConfigDialog-B4RDMLQ7.js → CopyClientConfigDialog-CDEiSInj.js} +2 -2
- package/frontend/dist/assets/{CopyClientConfigDialog-B4RDMLQ7.js.map → CopyClientConfigDialog-CDEiSInj.js.map} +1 -1
- package/frontend/dist/assets/CredentialsPage-BCLHY_fN.js +2 -0
- package/frontend/dist/assets/CredentialsPage-BCLHY_fN.js.map +1 -0
- package/frontend/dist/assets/{Dashboard-Bp8ZGDp5.js → Dashboard-B3Y6nAFL.js} +2 -2
- package/frontend/dist/assets/{Dashboard-Bp8ZGDp5.js.map → Dashboard-B3Y6nAFL.js.map} +1 -1
- package/frontend/dist/assets/{EndpointCopy-CH7rfwDL.js → EndpointCopy-BMxT99Ne.js} +2 -2
- package/frontend/dist/assets/{EndpointCopy-CH7rfwDL.js.map → EndpointCopy-BMxT99Ne.js.map} +1 -1
- package/frontend/dist/assets/GroupsPage-5iCk1LKT.js +33 -0
- package/frontend/dist/assets/GroupsPage-5iCk1LKT.js.map +1 -0
- package/frontend/dist/assets/LoginPage-BjYzY6RH.js +2 -0
- package/frontend/dist/assets/LoginPage-BjYzY6RH.js.map +1 -0
- package/frontend/dist/assets/{LogsPage-B4RyT3TS.js → LogsPage-CGcFNKJn.js} +2 -2
- package/frontend/dist/assets/{LogsPage-B4RyT3TS.js.map → LogsPage-CGcFNKJn.js.map} +1 -1
- package/frontend/dist/assets/{MarketPage-DV7Bs6bj.js → MarketPage-B7JPqFw3.js} +2 -2
- package/frontend/dist/assets/{MarketPage-DV7Bs6bj.js.map → MarketPage-B7JPqFw3.js.map} +1 -1
- package/frontend/dist/assets/{OAuthConsentPage-DdxMIqDO.js → OAuthConsentPage-BYi_kgNj.js} +2 -2
- package/frontend/dist/assets/{OAuthConsentPage-DdxMIqDO.js.map → OAuthConsentPage-BYi_kgNj.js.map} +1 -1
- package/frontend/dist/assets/{PromptsPage-61Uj8gCp.js → PromptsPage-x6X5trLX.js} +2 -2
- package/frontend/dist/assets/{PromptsPage-61Uj8gCp.js.map → PromptsPage-x6X5trLX.js.map} +1 -1
- package/frontend/dist/assets/{ResourcesPage-BSgOwVld.js → ResourcesPage-BJZCPBs8.js} +2 -2
- package/frontend/dist/assets/{ResourcesPage-BSgOwVld.js.map → ResourcesPage-BJZCPBs8.js.map} +1 -1
- package/frontend/dist/assets/ServersPage-CsyGLden.js +39 -0
- package/frontend/dist/assets/ServersPage-CsyGLden.js.map +1 -0
- package/frontend/dist/assets/{SettingsPage-C8zYH_jq.js → SettingsPage-C53DI0Sz.js} +6 -6
- package/frontend/dist/assets/SettingsPage-C53DI0Sz.js.map +1 -0
- package/frontend/dist/assets/{StatusDot-R86yLB-f.js → StatusDot-BDJd3Iix.js} +2 -2
- package/frontend/dist/assets/{StatusDot-R86yLB-f.js.map → StatusDot-BDJd3Iix.js.map} +1 -1
- package/frontend/dist/assets/{ToggleGroup-DN1ttokb.js → ToggleGroup-BbmRhILg.js} +2 -2
- package/frontend/dist/assets/{ToggleGroup-DN1ttokb.js.map → ToggleGroup-BbmRhILg.js.map} +1 -1
- package/frontend/dist/assets/{UsersPage-BCMjm9JG.js → UsersPage-NUG6vF8C.js} +2 -2
- package/frontend/dist/assets/{UsersPage-BCMjm9JG.js.map → UsersPage-NUG6vF8C.js.map} +1 -1
- package/frontend/dist/assets/{clipboard-DjzVWhkB.js → clipboard-7BggsilD.js} +2 -2
- package/frontend/dist/assets/{clipboard-DjzVWhkB.js.map → clipboard-7BggsilD.js.map} +1 -1
- package/frontend/dist/assets/index-B_IITM9e.js +3 -0
- package/frontend/dist/assets/index-B_IITM9e.js.map +1 -0
- package/frontend/dist/assets/index-CnqLKewL.css +1 -0
- package/frontend/dist/assets/{resourceService-DRZl8_Vm.js → resourceService-B674Fopr.js} +2 -2
- package/frontend/dist/assets/{resourceService-DRZl8_Vm.js.map → resourceService-B674Fopr.js.map} +1 -1
- package/frontend/dist/assets/{useCostData-DVLGVcuy.js → useCostData-8Z1HjAL5.js} +2 -2
- package/frontend/dist/assets/{useCostData-DVLGVcuy.js.map → useCostData-8Z1HjAL5.js.map} +1 -1
- package/frontend/dist/assets/variableDetection-C0nP3Jj2.js +18 -0
- package/frontend/dist/assets/variableDetection-C0nP3Jj2.js.map +1 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +2 -2
- package/frontend/dist/assets/CredentialsPage-46Z8Qv7O.js +0 -2
- package/frontend/dist/assets/CredentialsPage-46Z8Qv7O.js.map +0 -1
- package/frontend/dist/assets/GroupsPage-DTjOja6T.js +0 -33
- package/frontend/dist/assets/GroupsPage-DTjOja6T.js.map +0 -1
- package/frontend/dist/assets/LoginPage-o6ibb_j3.js +0 -2
- package/frontend/dist/assets/LoginPage-o6ibb_j3.js.map +0 -1
- package/frontend/dist/assets/ServersPage-DYumMhzV.js +0 -39
- package/frontend/dist/assets/ServersPage-DYumMhzV.js.map +0 -1
- package/frontend/dist/assets/SettingsPage-C8zYH_jq.js.map +0 -1
- package/frontend/dist/assets/index--NhF_yyy.css +0 -1
- package/frontend/dist/assets/index-P_O2kAcB.js +0 -3
- package/frontend/dist/assets/index-P_O2kAcB.js.map +0 -1
- package/frontend/dist/assets/variableDetection-DiE4h0o7.js +0 -16
- package/frontend/dist/assets/variableDetection-DiE4h0o7.js.map +0 -1
|
@@ -1,19 +1,22 @@
|
|
|
1
|
+
import { isSafePassthroughHeader } from '../utils/passthroughHeaders.js';
|
|
1
2
|
import { observeToolCall, setMetricsServer } from './metricsService.js';
|
|
2
3
|
import { hasPersonalOAuth, PersonalOAuthContext, personalOAuthContexts, personalAuthorizationRequired, } from './personalOAuthService.js';
|
|
3
4
|
import { preserveListFreshness, combineListFreshness, remainingListTtl, } from '../utils/listFreshness.js';
|
|
4
5
|
import { expandServerConfig, getServerEnvironment } from './serverConfigEnvironment.js';
|
|
5
6
|
import { getMcpRequestGroup } from '../utils/mcpRequestGroup.js';
|
|
6
7
|
import { LegacyMcpClient } from '../clients/legacyMcpClient.js';
|
|
8
|
+
import { getElicitationCapabilities, installElicitationRelay, isElicitationClientClosed, withElicitation, } from './elicitationService.js';
|
|
7
9
|
import { LEGACY_PROTOCOL_VERSIONS } from '../utils/mcpProtocol.js';
|
|
8
10
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
9
11
|
import { createHash } from 'node:crypto';
|
|
10
12
|
import { canAccessGroupRoute } from '../utils/groupAccess.js';
|
|
11
13
|
import { canServeToolRequests } from '../utils/serverAvailability.js';
|
|
12
14
|
import { assertToolCallArgumentsSafe, isToolCallGuardEnabled, ToolCallBlockedError, } from '../middlewares/toolCallGuard.js';
|
|
15
|
+
import { applyHiddenParams, findToolConfig, removeHiddenArguments, } from '../utils/toolConfig.js';
|
|
13
16
|
import { PrincipalRuntimeService } from './principalRuntimeService.js';
|
|
14
17
|
import { UserContextService } from './userContextService.js';
|
|
15
18
|
import { deleteCredentialBindings, missingCredentialError } from './credentialBindingService.js';
|
|
16
|
-
import { CredentialBindingError, hasCredentialTemplate, hasPersonalAuthentication, resolveCredentialArguments, validateCredentialTemplate, } from '../utils/credentialTemplate.js';
|
|
19
|
+
import { CredentialBindingError, hasCredentialTemplate, hasPersonalAuthentication, resolveCredentialArguments, hasUrlCredentialSlots, validateCredentialTemplate, } from '../utils/credentialTemplate.js';
|
|
17
20
|
import os from 'os';
|
|
18
21
|
import path from 'path';
|
|
19
22
|
import fs from 'fs';
|
|
@@ -353,12 +356,12 @@ const setSessionIsolatedClient = (sessionId, serverName, client, transport) => {
|
|
|
353
356
|
* Used when the server has `perSessionClient: true` in its config.
|
|
354
357
|
* Uses a creation lock to prevent duplicate connections from concurrent calls.
|
|
355
358
|
*/
|
|
356
|
-
const getOrCreateIsolatedClient = async (sessionId, serverInfo) => {
|
|
359
|
+
const getOrCreateIsolatedClient = async (sessionId, serverInfo, downstream) => {
|
|
357
360
|
// Quick check without lock — already exists
|
|
358
361
|
const sessionClients = sessionIsolatedClients.get(sessionId);
|
|
359
362
|
if (sessionClients) {
|
|
360
363
|
const existing = sessionClients.get(serverInfo.name);
|
|
361
|
-
if (existing) {
|
|
364
|
+
if (existing && !isElicitationClientClosed(existing.client)) {
|
|
362
365
|
return existing;
|
|
363
366
|
}
|
|
364
367
|
}
|
|
@@ -373,7 +376,7 @@ const getOrCreateIsolatedClient = async (sessionId, serverInfo) => {
|
|
|
373
376
|
const sessionClientsAfterLock = sessionIsolatedClients.get(sessionId);
|
|
374
377
|
if (sessionClientsAfterLock) {
|
|
375
378
|
const existing = sessionClientsAfterLock.get(serverInfo.name);
|
|
376
|
-
if (existing) {
|
|
379
|
+
if (existing && !isElicitationClientClosed(existing.client)) {
|
|
377
380
|
return existing;
|
|
378
381
|
}
|
|
379
382
|
}
|
|
@@ -386,7 +389,7 @@ const getOrCreateIsolatedClient = async (sessionId, serverInfo) => {
|
|
|
386
389
|
}
|
|
387
390
|
const reservedClients = sessionIsolatedClients.get(sessionId);
|
|
388
391
|
const transport = await createTransportFromConfig(serverInfo.name, serverConfig);
|
|
389
|
-
const client = createUpstreamMcpClient(serverInfo.name, () => serverInfo);
|
|
392
|
+
const client = createUpstreamMcpClient(serverInfo.name, () => serverInfo, downstream);
|
|
390
393
|
try {
|
|
391
394
|
await connectClientWithDiagnostics(client, transport, serverInfo.options || {});
|
|
392
395
|
}
|
|
@@ -505,6 +508,8 @@ const hasDescriptionOverride = (config) => {
|
|
|
505
508
|
const resolveDescriptionOverride = (defaultDescription, config) => {
|
|
506
509
|
return hasDescriptionOverride(config) ? config?.description || '' : defaultDescription || '';
|
|
507
510
|
};
|
|
511
|
+
// The dashboard/API projection keeps the full input schema (the dashboard
|
|
512
|
+
// lists every parameter to choose what to hide) and reports the configured list.
|
|
508
513
|
const buildToolWithDescriptionMetadata = (tool, toolConfig) => {
|
|
509
514
|
const upstreamDescription = tool.description || '';
|
|
510
515
|
return {
|
|
@@ -512,13 +517,15 @@ const buildToolWithDescriptionMetadata = (tool, toolConfig) => {
|
|
|
512
517
|
description: resolveDescriptionOverride(upstreamDescription, toolConfig),
|
|
513
518
|
defaultDescription: upstreamDescription,
|
|
514
519
|
hasDescriptionOverride: hasDescriptionOverride(toolConfig),
|
|
520
|
+
...(toolConfig?.hiddenParams?.length ? { hiddenParams: toolConfig.hiddenParams } : {}),
|
|
515
521
|
enabled: toolConfig?.enabled !== false,
|
|
516
522
|
};
|
|
517
523
|
};
|
|
518
|
-
// Apply any per-tool description override before the tool
|
|
519
|
-
// Smart Routing search. Deliberately narrower than
|
|
520
|
-
// only `description` (plus the
|
|
521
|
-
// tool-set hash)
|
|
524
|
+
// Apply any per-tool description override and hidden parameters before the tool
|
|
525
|
+
// text is embedded for Smart Routing search. Deliberately narrower than
|
|
526
|
+
// buildToolWithDescriptionMetadata: only `description` (plus the
|
|
527
|
+
// hasDescriptionOverride marker used by the tool-set hash) and `inputSchema`
|
|
528
|
+
// are touched, so serverInfo.tools (built by
|
|
522
529
|
// normalizeToolForCache/updateServerToolsCache) still never carries `enabled` --
|
|
523
530
|
// preserving the existing invariant that execution gating reads the persisted
|
|
524
531
|
// config directly, not this projection. Without this step, overrides only
|
|
@@ -529,16 +536,18 @@ const applyDescriptionOverridesForEmbedding = async (serverName, tools) => {
|
|
|
529
536
|
if (!serverConfig?.tools) {
|
|
530
537
|
return tools;
|
|
531
538
|
}
|
|
539
|
+
// A partial sync passes only some tools; the key rule needs all of them
|
|
540
|
+
const runtimeNames = (getServerByName(serverName)?.tools ?? tools).map((tool) => tool.name);
|
|
532
541
|
return tools.map((tool) => {
|
|
533
|
-
const
|
|
534
|
-
const
|
|
542
|
+
const toolConfig = findToolConfig(serverConfig.tools, serverName, tool.name, runtimeNames);
|
|
543
|
+
const visibleTool = applyHiddenParams(tool, toolConfig);
|
|
535
544
|
return hasDescriptionOverride(toolConfig)
|
|
536
545
|
? {
|
|
537
|
-
...
|
|
546
|
+
...visibleTool,
|
|
538
547
|
description: resolveDescriptionOverride(tool.description, toolConfig),
|
|
539
548
|
hasDescriptionOverride: true,
|
|
540
549
|
}
|
|
541
|
-
:
|
|
550
|
+
: visibleTool;
|
|
542
551
|
});
|
|
543
552
|
};
|
|
544
553
|
// Tail of the embedding tasks queued per server. A per-user connect and a
|
|
@@ -832,12 +841,13 @@ const updateServerResourcesCache = (serverInfo, resources) => {
|
|
|
832
841
|
const logListChangedRefreshError = (listType) => {
|
|
833
842
|
logger.warn(`Failed to refresh ${listType} list after upstream notification`);
|
|
834
843
|
};
|
|
835
|
-
const createUpstreamMcpClient = (name, getServerInfo) => {
|
|
836
|
-
|
|
844
|
+
const createUpstreamMcpClient = (name, getServerInfo, downstream) => {
|
|
845
|
+
const elicitation = getElicitationCapabilities(downstream);
|
|
846
|
+
const client = new LegacyMcpClient({
|
|
837
847
|
name: `mcp-client-${name}`,
|
|
838
848
|
version: '1.0.0',
|
|
839
849
|
}, {
|
|
840
|
-
capabilities: MCP_APPS_CAPABILITIES,
|
|
850
|
+
capabilities: { ...MCP_APPS_CAPABILITIES, ...(elicitation ? { elicitation } : {}) },
|
|
841
851
|
jsonSchemaValidator: new ResilientJsonSchemaValidator(),
|
|
842
852
|
listChanged: {
|
|
843
853
|
tools: {
|
|
@@ -887,6 +897,9 @@ const createUpstreamMcpClient = (name, getServerInfo) => {
|
|
|
887
897
|
},
|
|
888
898
|
},
|
|
889
899
|
});
|
|
900
|
+
if (elicitation && downstream)
|
|
901
|
+
installElicitationRelay(client, downstream);
|
|
902
|
+
return client;
|
|
890
903
|
};
|
|
891
904
|
// Normalize and infer server type for safe client display
|
|
892
905
|
const normalizeServerType = (type) => {
|
|
@@ -1162,7 +1175,7 @@ export const collectPassthroughHeaders = (requestHeaders, passthroughHeaderNames
|
|
|
1162
1175
|
const passthroughHeaders = {};
|
|
1163
1176
|
for (const headerName of passthroughHeaderNames) {
|
|
1164
1177
|
const normalizedHeaderName = headerName.trim();
|
|
1165
|
-
if (!normalizedHeaderName) {
|
|
1178
|
+
if (!isSafePassthroughHeader(normalizedHeaderName)) {
|
|
1166
1179
|
continue;
|
|
1167
1180
|
}
|
|
1168
1181
|
const headerValue = headerValueToString(getHeaderValue(requestHeaders, normalizedHeaderName));
|
|
@@ -1221,7 +1234,7 @@ export const createTransportFromConfig = async (name, conf) => {
|
|
|
1221
1234
|
// skip the internal-IP blocklist. allowInternal also governs per-hop
|
|
1222
1235
|
// redirect validation in createRedirectValidatingFetch below.
|
|
1223
1236
|
const ownerUser = conf.owner ? await getUserDao().findByUsername(conf.owner) : null;
|
|
1224
|
-
const allowInternal = !!ownerUser?.isAdmin;
|
|
1237
|
+
const allowInternal = !!ownerUser?.isAdmin && !hasUrlCredentialSlots(conf);
|
|
1225
1238
|
if (conf.url) {
|
|
1226
1239
|
await assertSafeUrl(conf.url, { allowInternal });
|
|
1227
1240
|
}
|
|
@@ -1351,8 +1364,18 @@ export const createTransportFromConfig = async (name, conf) => {
|
|
|
1351
1364
|
}
|
|
1352
1365
|
return transport;
|
|
1353
1366
|
};
|
|
1367
|
+
// Call arguments without the tool's hidden parameters; `toolName` is the bare upstream name.
|
|
1368
|
+
// Looked up by the cached runtime name: findToolConfig removes one server prefix,
|
|
1369
|
+
// so a bare name that itself starts with the prefix (server `fc`, tool
|
|
1370
|
+
// `fc-scrape`) would otherwise resolve to the wrong entry.
|
|
1371
|
+
const withoutHiddenArguments = (serverInfo, toolName, args) => {
|
|
1372
|
+
const runtimeName = `${serverInfo.name}${getNameSeparator()}${toolName}`;
|
|
1373
|
+
const tool = serverInfo.tools?.find((candidate) => candidate.name === runtimeName);
|
|
1374
|
+
const toolConfig = findToolConfig(serverInfo.config?.tools, serverInfo.name, runtimeName, (serverInfo.tools ?? []).map((candidate) => candidate.name));
|
|
1375
|
+
return removeHiddenArguments(args, tool?.inputSchema, toolConfig);
|
|
1376
|
+
};
|
|
1354
1377
|
// Helper function to handle client.callTool with reconnection logic
|
|
1355
|
-
const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries = 1, isolated) => {
|
|
1378
|
+
const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries = 1, isolated, extra) => {
|
|
1356
1379
|
// Local, reassignable refs so a reconnect can swap in the new connection for
|
|
1357
1380
|
// the next retry attempt. For isolated calls these track the per-session
|
|
1358
1381
|
// client/transport; otherwise they mirror the shared serverInfo connection.
|
|
@@ -1361,17 +1384,33 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
|
|
|
1361
1384
|
if (!client) {
|
|
1362
1385
|
throw new Error(`Client not found for server: ${serverInfo.name}`);
|
|
1363
1386
|
}
|
|
1387
|
+
// Both MCP call paths pass through here, so hidden parameters are dropped once.
|
|
1388
|
+
toolParams = {
|
|
1389
|
+
...toolParams,
|
|
1390
|
+
arguments: withoutHiddenArguments(serverInfo, toolParams.name, toolParams.arguments),
|
|
1391
|
+
};
|
|
1364
1392
|
if (hasPersonalAuthentication(serverInfo.config)) {
|
|
1365
1393
|
return client.callTool(toolParams, options || {});
|
|
1366
1394
|
}
|
|
1367
1395
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1368
1396
|
try {
|
|
1369
|
-
const
|
|
1370
|
-
|
|
1371
|
-
|
|
1397
|
+
const currentClient = client;
|
|
1398
|
+
const result = await withElicitation(currentClient, extra?.mcpReq, options || {}, async (callOptions) => {
|
|
1399
|
+
const result = await currentClient.callTool(toolParams, callOptions);
|
|
1400
|
+
checkAuthError(result);
|
|
1401
|
+
return result;
|
|
1402
|
+
});
|
|
1372
1403
|
return result;
|
|
1373
1404
|
}
|
|
1374
1405
|
catch (error) {
|
|
1406
|
+
if (isElicitationClientClosed(client)) {
|
|
1407
|
+
closeIsolatedClient(serverInfo.name, client, transport);
|
|
1408
|
+
throw error;
|
|
1409
|
+
}
|
|
1410
|
+
// A cancelled queued call never reached the upstream. In particular, do
|
|
1411
|
+
// not let the generic SSE retry replace its healthy isolated connection.
|
|
1412
|
+
if (extra?.mcpReq?.signal.aborted)
|
|
1413
|
+
throw error;
|
|
1375
1414
|
const isHttp40xError = isRecoverableHttp4xxError(error);
|
|
1376
1415
|
// Only retry for StreamableHTTPClientTransport and SSE transports
|
|
1377
1416
|
const isStreamableHttp = transport instanceof StreamableHTTPClientTransport;
|
|
@@ -2094,11 +2133,8 @@ export const getServersInfo = async (page, limit, user) => {
|
|
|
2094
2133
|
// as the dashboard sends, or bare, as a direct API call may) with no
|
|
2095
2134
|
// normalization — check both so the dashboard reflects a disable set
|
|
2096
2135
|
// either way instead of always showing enabled:true for one of them.
|
|
2097
|
-
const
|
|
2098
|
-
|
|
2099
|
-
const toolConfig = serverConfig?.tools?.[tool.name] ?? serverConfig?.tools?.[bareToolName];
|
|
2100
|
-
return buildToolWithDescriptionMetadata(tool, toolConfig);
|
|
2101
|
-
});
|
|
2136
|
+
const runtimeNames = tools.map((tool) => tool.name);
|
|
2137
|
+
const toolsWithEnabled = tools.map((tool) => buildToolWithDescriptionMetadata(tool, findToolConfig(serverConfig?.tools, name, tool.name, runtimeNames)));
|
|
2102
2138
|
const promptsWithEnabled = prompts.map((prompt) => {
|
|
2103
2139
|
const promptConfig = serverConfig?.prompts?.[prompt.name];
|
|
2104
2140
|
return {
|
|
@@ -2339,12 +2375,12 @@ const filterToolsByConfig = async (serverName, tools) => {
|
|
|
2339
2375
|
// If no tool configuration exists, all tools are enabled by default
|
|
2340
2376
|
return tools;
|
|
2341
2377
|
}
|
|
2378
|
+
// Callers may pass a subset; the key rule needs every tool of the server
|
|
2379
|
+
const runtimeNames = (getServerByName(serverName)?.tools ?? tools).map((tool) => tool.name);
|
|
2342
2380
|
return tools.filter((tool) => {
|
|
2343
|
-
//
|
|
2344
|
-
//
|
|
2345
|
-
|
|
2346
|
-
const bareToolName = normalizeToolNameForServer(serverName, tool.name);
|
|
2347
|
-
const toolConfig = serverConfig.tools?.[tool.name] ?? serverConfig.tools?.[bareToolName];
|
|
2381
|
+
// A disable may be stored under the runtime or the bare name — same key rule
|
|
2382
|
+
// as findToolOnServer's execution-time gate (findToolConfig).
|
|
2383
|
+
const toolConfig = findToolConfig(serverConfig.tools, serverName, tool.name, runtimeNames);
|
|
2348
2384
|
// If tool is not in config, it's enabled by default
|
|
2349
2385
|
return toolConfig?.enabled !== false;
|
|
2350
2386
|
});
|
|
@@ -2920,24 +2956,25 @@ const getMcpAppsRouteContext = async (sessionId, group) => {
|
|
|
2920
2956
|
return eligible.length > 0 ? { enabled: true, serverInfos: eligible } : { enabled: false };
|
|
2921
2957
|
}
|
|
2922
2958
|
const { filteredServerInfos, serverConfigsByName } = await getFilteredServerInfosForGroup(group);
|
|
2923
|
-
const
|
|
2924
|
-
|
|
2959
|
+
const eligibleServerInfos = filteredServerInfos.filter((serverInfo) => canServeToolRequests(serverInfo) &&
|
|
2960
|
+
(!!serverInfo.client || serverInfo.config?.startOnDemand === true));
|
|
2961
|
+
if (eligibleServerInfos.length === 0) {
|
|
2925
2962
|
return { enabled: false };
|
|
2926
2963
|
}
|
|
2927
2964
|
// Connection availability must not turn an aggregate route into a raw-name
|
|
2928
2965
|
// route: OpenAPI uses openApiClient, and tool calls acquire only the target
|
|
2929
2966
|
// personal runtime. Match resolveToolInGroup's configured-route ambiguity check.
|
|
2930
|
-
if (
|
|
2967
|
+
if (eligibleServerInfos.length === 1 &&
|
|
2931
2968
|
filteredServerInfos.length === 1 &&
|
|
2932
2969
|
serverConfigsByName.size <= 1) {
|
|
2933
2970
|
return {
|
|
2934
2971
|
enabled: true,
|
|
2935
|
-
serverInfo:
|
|
2972
|
+
serverInfo: eligibleServerInfos[0],
|
|
2936
2973
|
};
|
|
2937
2974
|
}
|
|
2938
2975
|
return {
|
|
2939
2976
|
enabled: true,
|
|
2940
|
-
serverInfos:
|
|
2977
|
+
serverInfos: eligibleServerInfos,
|
|
2941
2978
|
};
|
|
2942
2979
|
};
|
|
2943
2980
|
const normalizeToolNameForServer = (serverName, toolName) => {
|
|
@@ -2972,9 +3009,14 @@ const projectNameForGroup = (name, serverName, serverConfig) => {
|
|
|
2972
3009
|
const resolveNameFromGroup = (name, serverName, serverConfig) => {
|
|
2973
3010
|
return replacePrefixedServerName(name, getExposedServerName(serverName, serverConfig), serverName);
|
|
2974
3011
|
};
|
|
2975
|
-
const findToolOnServer = (serverInfo, toolName, allowRawName) => {
|
|
2976
|
-
|
|
2977
|
-
|
|
3012
|
+
const findToolOnServer = (serverInfo, toolName, allowRawName, preferRawName = false) => {
|
|
3013
|
+
// A listed upstream name takes precedence over a colliding legacy cache alias.
|
|
3014
|
+
const rawTool = preferRawName && allowRawName
|
|
3015
|
+
? serverInfo.tools.find((tool) => normalizeToolNameForServer(serverInfo.name, tool.name) === toolName)
|
|
3016
|
+
: undefined;
|
|
3017
|
+
const tool = rawTool ||
|
|
3018
|
+
serverInfo.tools.find((tool) => tool.name === toolName ||
|
|
3019
|
+
(allowRawName && normalizeToolNameForServer(serverInfo.name, tool.name) === toolName));
|
|
2978
3020
|
if (!tool) {
|
|
2979
3021
|
return undefined;
|
|
2980
3022
|
}
|
|
@@ -2989,16 +3031,11 @@ const findToolOnServer = (serverInfo, toolName, allowRawName) => {
|
|
|
2989
3031
|
// which is a different object. So the persisted per-tool config must be
|
|
2990
3032
|
// consulted directly here.
|
|
2991
3033
|
//
|
|
2992
|
-
//
|
|
2993
|
-
//
|
|
2994
|
-
//
|
|
2995
|
-
//
|
|
2996
|
-
|
|
2997
|
-
// pass the bare upstream name, which then never matches a prefixed-only
|
|
2998
|
-
// lookup. Check both so a disable takes effect regardless of which form
|
|
2999
|
-
// was used to set it.
|
|
3000
|
-
const bareToolName = normalizeToolNameForServer(serverInfo.name, tool.name);
|
|
3001
|
-
const toolConfig = serverInfo.config?.tools?.[tool.name] ?? serverInfo.config?.tools?.[bareToolName];
|
|
3034
|
+
// A disable may be stored under the runtime (prefixed) name, as the
|
|
3035
|
+
// dashboard writes it, or under the bare upstream name, as a settings file
|
|
3036
|
+
// or a direct API call may. findToolConfig checks both, and never reads
|
|
3037
|
+
// another tool's runtime key as this tool's bare name.
|
|
3038
|
+
const toolConfig = findToolConfig(serverInfo.config?.tools, serverInfo.name, tool.name, serverInfo.tools.map((candidate) => candidate.name));
|
|
3002
3039
|
if (toolConfig && toolConfig.enabled === false) {
|
|
3003
3040
|
return undefined;
|
|
3004
3041
|
}
|
|
@@ -3028,7 +3065,7 @@ const resolveToolInGroup = async (group, toolName) => {
|
|
|
3028
3065
|
}
|
|
3029
3066
|
const serverConfig = serverConfigsByName.get(serverInfo.name);
|
|
3030
3067
|
const internalToolName = resolveNameFromGroup(toolName, serverInfo.name, serverConfig);
|
|
3031
|
-
const tool = findToolOnServer(serverInfo, internalToolName, allowRawName);
|
|
3068
|
+
const tool = findToolOnServer(serverInfo, internalToolName, allowRawName, serverConfigsByName.size === 0);
|
|
3032
3069
|
if (!tool) {
|
|
3033
3070
|
continue;
|
|
3034
3071
|
}
|
|
@@ -3040,6 +3077,17 @@ const resolveToolInGroup = async (group, toolName) => {
|
|
|
3040
3077
|
}
|
|
3041
3078
|
return undefined;
|
|
3042
3079
|
};
|
|
3080
|
+
const findEnabledPromptOnServer = async (serverInfo, name, allowRawName = false) => {
|
|
3081
|
+
if (serverInfo.status !== 'connected' || serverInfo.enabled === false)
|
|
3082
|
+
return undefined;
|
|
3083
|
+
const prompt = serverInfo.prompts.find((item) => item.name === name ||
|
|
3084
|
+
(allowRawName && item.name === `${serverInfo.name}${getNameSeparator()}${name}`));
|
|
3085
|
+
if (!prompt)
|
|
3086
|
+
return undefined;
|
|
3087
|
+
const config = await getServerDao().findById(serverInfo.name);
|
|
3088
|
+
const promptConfig = findToolConfig(config?.prompts, serverInfo.name, prompt.name, serverInfo.prompts.map((item) => item.name));
|
|
3089
|
+
return config?.enabled === false || promptConfig?.enabled === false ? undefined : prompt;
|
|
3090
|
+
};
|
|
3043
3091
|
async function resolvePromptInGroup(group, promptName) {
|
|
3044
3092
|
const lookupGroup = getGroupLookupName(group);
|
|
3045
3093
|
if (!lookupGroup) {
|
|
@@ -3052,7 +3100,7 @@ async function resolvePromptInGroup(group, promptName) {
|
|
|
3052
3100
|
}
|
|
3053
3101
|
const serverConfig = serverConfigsByName.get(serverInfo.name);
|
|
3054
3102
|
const internalPromptName = resolveNameFromGroup(promptName, serverInfo.name, serverConfig);
|
|
3055
|
-
const prompt = serverInfo
|
|
3103
|
+
const prompt = await findEnabledPromptOnServer(serverInfo, internalPromptName);
|
|
3056
3104
|
if (!prompt) {
|
|
3057
3105
|
continue;
|
|
3058
3106
|
}
|
|
@@ -3064,21 +3112,19 @@ async function resolvePromptInGroup(group, promptName) {
|
|
|
3064
3112
|
}
|
|
3065
3113
|
return undefined;
|
|
3066
3114
|
}
|
|
3067
|
-
const projectToolForDownstream = (serverName, tool, appsRouteContext, serverConfig) => {
|
|
3115
|
+
const projectToolForDownstream = (serverName, tool, appsRouteContext, serverConfig, useRawName = false) => {
|
|
3068
3116
|
if (!appsRouteContext.enabled && isAppOnlyTool(tool)) {
|
|
3069
3117
|
return undefined;
|
|
3070
3118
|
}
|
|
3071
3119
|
const projectedTool = appsRouteContext.enabled ? tool : stripMcpAppsMetadata(tool);
|
|
3072
|
-
//
|
|
3073
|
-
|
|
3074
|
-
if (appsRouteContext.serverInfo) {
|
|
3120
|
+
// Naming follows the route, independently of Apps capabilities or connection state.
|
|
3121
|
+
if (useRawName) {
|
|
3075
3122
|
return {
|
|
3076
3123
|
...projectedTool,
|
|
3077
3124
|
name: normalizeToolNameForServer(serverName, projectedTool.name),
|
|
3078
3125
|
};
|
|
3079
3126
|
}
|
|
3080
|
-
//
|
|
3081
|
-
// name so it can still be routed to the right server unambiguously.
|
|
3127
|
+
// Group and aggregate routes keep qualified names, including single-member groups.
|
|
3082
3128
|
return {
|
|
3083
3129
|
...projectedTool,
|
|
3084
3130
|
name: projectNameForGroup(projectedTool.name, serverName, serverConfig),
|
|
@@ -3091,6 +3137,7 @@ const listGroupTools = async (group, filteredServerInfos, serverConfigsByName, a
|
|
|
3091
3137
|
await Promise.allSettled(filteredServerInfos
|
|
3092
3138
|
.filter((si) => si.config?.startOnDemand === true && si.tools.length === 0 && si.spawningPromise)
|
|
3093
3139
|
.map((si) => si.spawningPromise));
|
|
3140
|
+
const useRawName = !!group && !isSmartRoutingGroup(group) && !(await findRouteGroup(group));
|
|
3094
3141
|
const allTools = [];
|
|
3095
3142
|
for (const serverInfo of filteredServerInfos) {
|
|
3096
3143
|
if (serverInfo.tools && serverInfo.tools.length > 0) {
|
|
@@ -3099,17 +3146,18 @@ const listGroupTools = async (group, filteredServerInfos, serverConfigsByName, a
|
|
|
3099
3146
|
let tools = await filterToolsByConfig(serverInfo.name, serverInfo.tools);
|
|
3100
3147
|
// If this is a group request, apply group-level tool filtering
|
|
3101
3148
|
tools = await filterToolsByGroup(group, serverInfo.name, tools, groupServerConfig);
|
|
3102
|
-
// Apply custom descriptions from server configuration
|
|
3149
|
+
// Apply custom descriptions and hidden parameters from server configuration
|
|
3103
3150
|
const serverConfig = await getServerDao().findById(serverInfo.name);
|
|
3151
|
+
const runtimeNames = serverInfo.tools.map((tool) => tool.name);
|
|
3104
3152
|
const toolsWithCustomDescriptions = tools.map((tool) => {
|
|
3105
|
-
const toolConfig = serverConfig?.tools
|
|
3153
|
+
const toolConfig = findToolConfig(serverConfig?.tools, serverInfo.name, tool.name, runtimeNames);
|
|
3106
3154
|
return {
|
|
3107
|
-
...tool,
|
|
3155
|
+
...applyHiddenParams(tool, toolConfig),
|
|
3108
3156
|
description: resolveDescriptionOverride(tool.description, toolConfig),
|
|
3109
3157
|
};
|
|
3110
3158
|
});
|
|
3111
3159
|
allTools.push(...toolsWithCustomDescriptions.flatMap((tool) => {
|
|
3112
|
-
const projectedTool = projectToolForDownstream(serverInfo.name, tool, appsRouteContext, groupServerConfig);
|
|
3160
|
+
const projectedTool = projectToolForDownstream(serverInfo.name, tool, appsRouteContext, groupServerConfig, useRawName);
|
|
3113
3161
|
return projectedTool ? [projectedTool] : [];
|
|
3114
3162
|
}));
|
|
3115
3163
|
}
|
|
@@ -3266,28 +3314,33 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3266
3314
|
};
|
|
3267
3315
|
try {
|
|
3268
3316
|
appsRouteContext = await getMcpAppsRouteContext(sessionId, group);
|
|
3317
|
+
const lookupRoute = getGroupLookupName(group);
|
|
3318
|
+
// Direct routes own their listed raw names, even when a tool is disabled.
|
|
3319
|
+
const upstreamOwnsMetaName = SMART_ROUTING_META_TOOL_NAMES.has(request.params.name) &&
|
|
3320
|
+
!!lookupRoute &&
|
|
3321
|
+
!isSmartRoutingGroup(group) &&
|
|
3322
|
+
!(await findRouteGroup(lookupRoute)) &&
|
|
3323
|
+
getVisibleServerByName(lookupRoute)?.tools.some((tool) => normalizeToolNameForServer(lookupRoute, tool.name) === request.params.name);
|
|
3269
3324
|
// Special handling for smart routing tools
|
|
3270
|
-
if (request.params.name === 'search_tools') {
|
|
3325
|
+
if (!upstreamOwnsMetaName && request.params.name === 'search_tools') {
|
|
3271
3326
|
const { query, limit = 10 } = request.params.arguments || {};
|
|
3272
3327
|
return await handleSearchToolsRequest(query, limit, sessionId, group);
|
|
3273
3328
|
}
|
|
3274
3329
|
// Special handling for describe_tool (progressive disclosure mode)
|
|
3275
|
-
if (request.params.name === 'describe_tool') {
|
|
3330
|
+
if (!upstreamOwnsMetaName && request.params.name === 'describe_tool') {
|
|
3276
3331
|
const { toolName } = request.params.arguments || {};
|
|
3277
3332
|
return await handleDescribeToolRequest(toolName, sessionId, group);
|
|
3278
3333
|
}
|
|
3279
3334
|
// Special handling for call_tool
|
|
3280
|
-
if (request.params.name === 'call_tool') {
|
|
3335
|
+
if (!upstreamOwnsMetaName && request.params.name === 'call_tool') {
|
|
3281
3336
|
const { toolName } = request.params.arguments || {};
|
|
3282
3337
|
if (!toolName) {
|
|
3283
3338
|
throw new Error('toolName parameter is required');
|
|
3284
3339
|
}
|
|
3285
3340
|
const { arguments: toolArgs } = request.params.arguments || {};
|
|
3286
|
-
//
|
|
3287
|
-
//
|
|
3288
|
-
|
|
3289
|
-
// resolution below, same as an ordinary (non-Apps) group route.
|
|
3290
|
-
const singleServerAppsRoute = !!appsRouteContext.serverInfo;
|
|
3341
|
+
// Preserve legacy raw aggregate calls; explicit routes use the shared
|
|
3342
|
+
// resolver so cached names still obey group filtering.
|
|
3343
|
+
const singleServerAppsRoute = !getGroupLookupName(group) && !!appsRouteContext.serverInfo;
|
|
3291
3344
|
let targetServerInfo;
|
|
3292
3345
|
let targetToolName = toolName;
|
|
3293
3346
|
let targetTool;
|
|
@@ -3348,7 +3401,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3348
3401
|
arguments: summarizeArgumentsForLogging(finalArgs),
|
|
3349
3402
|
});
|
|
3350
3403
|
// Remove server prefix from tool name if present
|
|
3351
|
-
const cleanToolName = normalizeToolNameForServer(targetServerInfo.name,
|
|
3404
|
+
const cleanToolName = normalizeToolNameForServer(targetServerInfo.name, tool.name);
|
|
3352
3405
|
// Extract passthrough headers from extra or request context
|
|
3353
3406
|
let passthroughHeaders;
|
|
3354
3407
|
let requestHeaders = null;
|
|
@@ -3364,6 +3417,8 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3364
3417
|
if (requestHeaders && targetServerInfo.config?.openapi?.passthroughHeaders) {
|
|
3365
3418
|
passthroughHeaders = {};
|
|
3366
3419
|
for (const headerName of targetServerInfo.config.openapi.passthroughHeaders) {
|
|
3420
|
+
if (!isSafePassthroughHeader(headerName))
|
|
3421
|
+
continue;
|
|
3367
3422
|
// Handle different header name cases (Express normalizes headers to lowercase)
|
|
3368
3423
|
const headerValue = requestHeaders[headerName] || requestHeaders[headerName.toLowerCase()];
|
|
3369
3424
|
if (headerValue) {
|
|
@@ -3380,7 +3435,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3380
3435
|
throw new Error(`OpenAPI server '${targetServerInfo.name}' requires cookie-session state and is not yet available on stateless MCP requests`);
|
|
3381
3436
|
}
|
|
3382
3437
|
await reserveHostedIfNeeded(targetServerInfo.name, cleanToolName);
|
|
3383
|
-
const result = await openApiClient.callTool(cleanToolName, finalArgs, passthroughHeaders, false, stateId);
|
|
3438
|
+
const result = await openApiClient.callTool(cleanToolName, withoutHiddenArguments(targetServerInfo, cleanToolName, finalArgs), passthroughHeaders, false, stateId);
|
|
3384
3439
|
await settleHostedIfNeeded({
|
|
3385
3440
|
success: true,
|
|
3386
3441
|
requestContent: finalArgs,
|
|
@@ -3429,7 +3484,9 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3429
3484
|
if (!stateId) {
|
|
3430
3485
|
throw new Error(`Server '${targetServerInfo.name}' requires per-session client isolation and is not yet available on stateless MCP requests`);
|
|
3431
3486
|
}
|
|
3432
|
-
const isolated = await getOrCreateIsolatedClient(stateId, targetServerInfo)
|
|
3487
|
+
const isolated = await getOrCreateIsolatedClient(stateId, targetServerInfo, extra.mcpReq && !requestContextService.getRequestContext()?.stateless
|
|
3488
|
+
? servers[sessionId]
|
|
3489
|
+
: undefined);
|
|
3433
3490
|
isolatedCtx = {
|
|
3434
3491
|
sessionId: stateId,
|
|
3435
3492
|
client: isolated.client,
|
|
@@ -3447,12 +3504,12 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3447
3504
|
arguments: summarizeArgumentsForLogging(finalArgs),
|
|
3448
3505
|
perSessionClient: !!targetServerInfo.config?.perSessionClient,
|
|
3449
3506
|
});
|
|
3450
|
-
const cleanToolName = normalizeToolNameForServer(targetServerInfo.name,
|
|
3507
|
+
const cleanToolName = normalizeToolNameForServer(targetServerInfo.name, tool.name);
|
|
3451
3508
|
await reserveHostedIfNeeded(targetServerInfo.name, cleanToolName);
|
|
3452
3509
|
const result = await callToolWithReconnect(targetServerInfo, {
|
|
3453
3510
|
name: cleanToolName,
|
|
3454
3511
|
arguments: finalArgs,
|
|
3455
|
-
}, targetServerInfo.options || {}, 1, isolatedCtx);
|
|
3512
|
+
}, targetServerInfo.options || {}, 1, isolatedCtx, extra);
|
|
3456
3513
|
await settleHostedIfNeeded({
|
|
3457
3514
|
success: !result.isError,
|
|
3458
3515
|
requestContent: finalArgs,
|
|
@@ -3488,13 +3545,9 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3488
3545
|
}
|
|
3489
3546
|
// Regular tool handling
|
|
3490
3547
|
const lookupGroup = getGroupLookupName(group);
|
|
3491
|
-
//
|
|
3492
|
-
//
|
|
3493
|
-
|
|
3494
|
-
// group lookup below, same as an ordinary (non-Apps) group route,
|
|
3495
|
-
// just without stripping Apps metadata or hiding app-only tools
|
|
3496
|
-
// (assertToolAvailableForRoute below still gates on appsRouteContext.enabled).
|
|
3497
|
-
const singleServerAppsRoute = !!appsRouteContext.serverInfo;
|
|
3548
|
+
// Explicit routes share name resolution and group filtering regardless of
|
|
3549
|
+
// Apps support; keep legacy raw aggregate calls compatible.
|
|
3550
|
+
const singleServerAppsRoute = !getGroupLookupName(group) && !!appsRouteContext.serverInfo;
|
|
3498
3551
|
const groupTool = !singleServerAppsRoute && lookupGroup
|
|
3499
3552
|
? await resolveToolInGroup(group, request.params.name)
|
|
3500
3553
|
: undefined;
|
|
@@ -3522,7 +3575,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3522
3575
|
// For OpenAPI servers, use the OpenAPI client
|
|
3523
3576
|
const openApiClient = serverInfo.openApiClient;
|
|
3524
3577
|
// Remove server prefix from tool name if present
|
|
3525
|
-
const cleanToolName = normalizeToolNameForServer(serverInfo.name,
|
|
3578
|
+
const cleanToolName = normalizeToolNameForServer(serverInfo.name, tool.name);
|
|
3526
3579
|
logger.log('Invoking OpenAPI tool', {
|
|
3527
3580
|
toolName: cleanToolName,
|
|
3528
3581
|
serverName: serverInfo.name,
|
|
@@ -3543,6 +3596,8 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3543
3596
|
if (requestHeaders && serverInfo.config?.openapi?.passthroughHeaders) {
|
|
3544
3597
|
passthroughHeaders = {};
|
|
3545
3598
|
for (const headerName of serverInfo.config.openapi.passthroughHeaders) {
|
|
3599
|
+
if (!isSafePassthroughHeader(headerName))
|
|
3600
|
+
continue;
|
|
3546
3601
|
// Handle different header name cases (Express normalizes headers to lowercase)
|
|
3547
3602
|
const headerValue = requestHeaders[headerName] || requestHeaders[headerName.toLowerCase()];
|
|
3548
3603
|
if (headerValue) {
|
|
@@ -3560,7 +3615,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3560
3615
|
throw new Error(`OpenAPI server '${serverInfo.name}' requires cookie-session state and is not yet available on stateless MCP requests`);
|
|
3561
3616
|
}
|
|
3562
3617
|
await reserveHostedIfNeeded(serverInfo.name, cleanToolName);
|
|
3563
|
-
const result = await openApiClient.callTool(cleanToolName, finalArgs, passthroughHeaders, false, stateId);
|
|
3618
|
+
const result = await openApiClient.callTool(cleanToolName, withoutHiddenArguments(serverInfo, cleanToolName, finalArgs), passthroughHeaders, false, stateId);
|
|
3564
3619
|
await settleHostedIfNeeded({
|
|
3565
3620
|
success: true,
|
|
3566
3621
|
requestContent: finalArgs,
|
|
@@ -3608,15 +3663,17 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3608
3663
|
if (!stateId) {
|
|
3609
3664
|
throw new Error(`Server '${serverInfo.name}' requires per-session client isolation and is not yet available on stateless MCP requests`);
|
|
3610
3665
|
}
|
|
3611
|
-
const isolated = await getOrCreateIsolatedClient(stateId, serverInfo)
|
|
3666
|
+
const isolated = await getOrCreateIsolatedClient(stateId, serverInfo, extra.mcpReq && !requestContextService.getRequestContext()?.stateless
|
|
3667
|
+
? servers[sessionId]
|
|
3668
|
+
: undefined);
|
|
3612
3669
|
isolatedCtx = { sessionId: stateId, client: isolated.client, transport: isolated.transport };
|
|
3613
3670
|
}
|
|
3614
3671
|
else if (!serverInfo.client) {
|
|
3615
3672
|
throw new Error(`Client not found for server: ${serverInfo.name}`);
|
|
3616
3673
|
}
|
|
3617
|
-
const cleanToolName = normalizeToolNameForServer(serverInfo.name,
|
|
3674
|
+
const cleanToolName = normalizeToolNameForServer(serverInfo.name, tool.name);
|
|
3618
3675
|
await reserveHostedIfNeeded(serverInfo.name, cleanToolName);
|
|
3619
|
-
const result = await callToolWithReconnect(serverInfo, { ...request.params, name: cleanToolName }, serverInfo.options || {}, 1, isolatedCtx);
|
|
3676
|
+
const result = await callToolWithReconnect(serverInfo, { ...request.params, name: cleanToolName }, serverInfo.options || {}, 1, isolatedCtx, extra);
|
|
3620
3677
|
await settleHostedIfNeeded({
|
|
3621
3678
|
success: !result.isError,
|
|
3622
3679
|
requestContent: request.params.arguments,
|
|
@@ -3715,7 +3772,7 @@ const handleGetPromptRequestImpl = async (request, extra) => {
|
|
|
3715
3772
|
const { name, arguments: promptArgs } = request.params;
|
|
3716
3773
|
const group = getMcpRequestGroup(extra);
|
|
3717
3774
|
// Check built-in prompts first
|
|
3718
|
-
const builtinPrompt = await getBuiltinPromptDao().findByName(name);
|
|
3775
|
+
const builtinPrompt = !extra?.server && (await getBuiltinPromptDao().findByName(name));
|
|
3719
3776
|
if (builtinPrompt && builtinPrompt.enabled !== false) {
|
|
3720
3777
|
// Perform {{param}} template substitution
|
|
3721
3778
|
let content = builtinPrompt.template;
|
|
@@ -3737,7 +3794,12 @@ const handleGetPromptRequestImpl = async (request, extra) => {
|
|
|
3737
3794
|
let promptNameForServer = name;
|
|
3738
3795
|
const lookupGroup = getGroupLookupName(group);
|
|
3739
3796
|
if (extra && extra.server) {
|
|
3740
|
-
|
|
3797
|
+
const candidate = getVisibleServerByName(extra.server);
|
|
3798
|
+
const prompt = candidate && (await findEnabledPromptOnServer(candidate, name, true));
|
|
3799
|
+
if (prompt) {
|
|
3800
|
+
server = candidate;
|
|
3801
|
+
promptNameForServer = prompt.name;
|
|
3802
|
+
}
|
|
3741
3803
|
}
|
|
3742
3804
|
else if (lookupGroup) {
|
|
3743
3805
|
const groupPrompt = await resolvePromptInGroup(lookupGroup, name);
|
|
@@ -3747,10 +3809,14 @@ const handleGetPromptRequestImpl = async (request, extra) => {
|
|
|
3747
3809
|
}
|
|
3748
3810
|
}
|
|
3749
3811
|
else {
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3812
|
+
for (const candidate of getVisibleServerInfos()) {
|
|
3813
|
+
const prompt = await findEnabledPromptOnServer(candidate, name);
|
|
3814
|
+
if (prompt) {
|
|
3815
|
+
server = candidate;
|
|
3816
|
+
promptNameForServer = prompt.name;
|
|
3817
|
+
break;
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3754
3820
|
}
|
|
3755
3821
|
if (!server) {
|
|
3756
3822
|
throw new ToolUnavailableError(`Prompt not available: ${name}`, classifyUnavailableReason(name));
|
|
@@ -3819,7 +3885,7 @@ const handleListPromptsRequestImpl = async (_, extra) => {
|
|
|
3819
3885
|
let enabledPrompts = serverInfo.prompts;
|
|
3820
3886
|
if (serverConfig && serverConfig.prompts) {
|
|
3821
3887
|
enabledPrompts = serverInfo.prompts.filter((prompt) => {
|
|
3822
|
-
const promptConfig = serverConfig.prompts
|
|
3888
|
+
const promptConfig = findToolConfig(serverConfig.prompts, serverInfo.name, prompt.name, serverInfo.prompts.map((item) => item.name));
|
|
3823
3889
|
// If prompt is not in config, it's enabled by default
|
|
3824
3890
|
return promptConfig?.enabled !== false;
|
|
3825
3891
|
});
|
|
@@ -3986,6 +4052,12 @@ const handleReadResourceRequestImpl = async (request, extra) => {
|
|
|
3986
4052
|
? [appsRouteContext.serverInfo]
|
|
3987
4053
|
: (appsRouteContext.serverInfos ?? []);
|
|
3988
4054
|
for (const candidate of candidates) {
|
|
4055
|
+
const config = await getServerDao().findById(candidate.name);
|
|
4056
|
+
if (config?.resources?.[uri]?.enabled === false)
|
|
4057
|
+
continue;
|
|
4058
|
+
const allowed = await filterResourcesByGroup(lookupGroup, candidate.name, [{ uri }], serverConfigsByName.get(candidate.name));
|
|
4059
|
+
if (allowed.length === 0)
|
|
4060
|
+
continue;
|
|
3989
4061
|
// An on-demand candidate may be idle yet still advertise its cached widget
|
|
3990
4062
|
// link, so a host can read the ui:// resource before any tool call has
|
|
3991
4063
|
// woken it. Hold off its idle shutdown for every read, awake or not, so an
|
|
@@ -4181,7 +4253,7 @@ const withPrincipalServers = (handler, operation) => async (request, extra) => {
|
|
|
4181
4253
|
if (!serverInfos.some((info) => hasPersonalAuthentication(info.config)))
|
|
4182
4254
|
return handler(request, extra);
|
|
4183
4255
|
// Built-in content does not require an upstream credential.
|
|
4184
|
-
if (operation === 'prompt') {
|
|
4256
|
+
if (operation === 'prompt' && !extra?.server) {
|
|
4185
4257
|
const builtin = await getBuiltinPromptDao().findByName(request.params?.name);
|
|
4186
4258
|
if (builtin && builtin.enabled !== false)
|
|
4187
4259
|
return handler(request, extra);
|