@samanhappy/mcphub 1.1.8 → 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/controllers/promptController.js +2 -2
- package/dist/controllers/promptController.js.map +1 -1
- package/dist/controllers/serverController.js +84 -1
- package/dist/controllers/serverController.js.map +1 -1
- package/dist/routes/index.js +2 -1
- package/dist/routes/index.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 +113 -61
- package/dist/services/mcpService.js.map +1 -1
- package/dist/services/serverConfigPresenter.js +6 -1
- package/dist/services/serverConfigPresenter.js.map +1 -1
- package/dist/services/sseService.js +1 -2
- package/dist/services/sseService.js.map +1 -1
- package/dist/services/templateService.js +2 -3
- package/dist/services/templateService.js.map +1 -1
- package/dist/utils/credentialTemplate.js +76 -6
- package/dist/utils/credentialTemplate.js.map +1 -1
- package/dist/utils/namePattern.js +28 -0
- package/dist/utils/namePattern.js.map +1 -0
- package/dist/utils/serverConfigPersistence.js +1 -3
- package/dist/utils/serverConfigPersistence.js.map +1 -1
- package/dist/utils/upstreamError.js +9 -2
- package/dist/utils/upstreamError.js.map +1 -1
- package/frontend/dist/assets/{ActivityPage-CJdAfOUF.js → ActivityPage-CFRX20v4.js} +2 -2
- package/frontend/dist/assets/{ActivityPage-CJdAfOUF.js.map → ActivityPage-CFRX20v4.js.map} +1 -1
- package/frontend/dist/assets/{CopyClientConfigDialog-DKktm8jj.js → CopyClientConfigDialog-CDEiSInj.js} +2 -2
- package/frontend/dist/assets/{CopyClientConfigDialog-DKktm8jj.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-DM_IiBIT.js → Dashboard-B3Y6nAFL.js} +2 -2
- package/frontend/dist/assets/{Dashboard-DM_IiBIT.js.map → Dashboard-B3Y6nAFL.js.map} +1 -1
- package/frontend/dist/assets/{EndpointCopy-DHQjJesJ.js → EndpointCopy-BMxT99Ne.js} +2 -2
- package/frontend/dist/assets/{EndpointCopy-DHQjJesJ.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-CKrI0By4.js → LoginPage-BjYzY6RH.js} +2 -2
- package/frontend/dist/assets/{LoginPage-CKrI0By4.js.map → LoginPage-BjYzY6RH.js.map} +1 -1
- package/frontend/dist/assets/{LogsPage-Dh8gRpEH.js → LogsPage-CGcFNKJn.js} +2 -2
- package/frontend/dist/assets/{LogsPage-Dh8gRpEH.js.map → LogsPage-CGcFNKJn.js.map} +1 -1
- package/frontend/dist/assets/{MarketPage-BN4qL8L9.js → MarketPage-B7JPqFw3.js} +2 -2
- package/frontend/dist/assets/{MarketPage-BN4qL8L9.js.map → MarketPage-B7JPqFw3.js.map} +1 -1
- package/frontend/dist/assets/{OAuthConsentPage-Cyj-rrA4.js → OAuthConsentPage-BYi_kgNj.js} +2 -2
- package/frontend/dist/assets/{OAuthConsentPage-Cyj-rrA4.js.map → OAuthConsentPage-BYi_kgNj.js.map} +1 -1
- package/frontend/dist/assets/{PromptsPage-YWD04XeI.js → PromptsPage-x6X5trLX.js} +2 -2
- package/frontend/dist/assets/{PromptsPage-YWD04XeI.js.map → PromptsPage-x6X5trLX.js.map} +1 -1
- package/frontend/dist/assets/{ResourcesPage-CVRzHCf4.js → ResourcesPage-BJZCPBs8.js} +2 -2
- package/frontend/dist/assets/{ResourcesPage-CVRzHCf4.js.map → ResourcesPage-BJZCPBs8.js.map} +1 -1
- package/frontend/dist/assets/{ServersPage-DJBurfOe.js → ServersPage-CsyGLden.js} +4 -4
- package/frontend/dist/assets/{ServersPage-DJBurfOe.js.map → ServersPage-CsyGLden.js.map} +1 -1
- package/frontend/dist/assets/{SettingsPage-3sK6NJRn.js → SettingsPage-C53DI0Sz.js} +2 -2
- package/frontend/dist/assets/{SettingsPage-3sK6NJRn.js.map → SettingsPage-C53DI0Sz.js.map} +1 -1
- package/frontend/dist/assets/{StatusDot-CLZidCyq.js → StatusDot-BDJd3Iix.js} +2 -2
- package/frontend/dist/assets/{StatusDot-CLZidCyq.js.map → StatusDot-BDJd3Iix.js.map} +1 -1
- package/frontend/dist/assets/{ToggleGroup-Dfh4jKpP.js → ToggleGroup-BbmRhILg.js} +2 -2
- package/frontend/dist/assets/{ToggleGroup-Dfh4jKpP.js.map → ToggleGroup-BbmRhILg.js.map} +1 -1
- package/frontend/dist/assets/{UsersPage-DfFqSRVb.js → UsersPage-NUG6vF8C.js} +2 -2
- package/frontend/dist/assets/{UsersPage-DfFqSRVb.js.map → UsersPage-NUG6vF8C.js.map} +1 -1
- package/frontend/dist/assets/{clipboard-wUZFI68F.js → clipboard-7BggsilD.js} +2 -2
- package/frontend/dist/assets/{clipboard-wUZFI68F.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-D7yitP7i.js → resourceService-B674Fopr.js} +2 -2
- package/frontend/dist/assets/{resourceService-D7yitP7i.js.map → resourceService-B674Fopr.js.map} +1 -1
- package/frontend/dist/assets/{useCostData-33G6s4EW.js → useCostData-8Z1HjAL5.js} +2 -2
- package/frontend/dist/assets/{useCostData-33G6s4EW.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 +1 -1
- package/frontend/dist/assets/CredentialsPage-CPyZrp4Z.js +0 -2
- package/frontend/dist/assets/CredentialsPage-CPyZrp4Z.js.map +0 -1
- package/frontend/dist/assets/GroupsPage-VKHjil-L.js +0 -33
- package/frontend/dist/assets/GroupsPage-VKHjil-L.js.map +0 -1
- package/frontend/dist/assets/index--NhF_yyy.css +0 -1
- package/frontend/dist/assets/index-BbogfcdA.js +0 -3
- package/frontend/dist/assets/index-BbogfcdA.js.map +0 -1
- package/frontend/dist/assets/variableDetection-BVSe56ia.js +0 -16
- package/frontend/dist/assets/variableDetection-BVSe56ia.js.map +0 -1
|
@@ -5,6 +5,7 @@ import { preserveListFreshness, combineListFreshness, remainingListTtl, } from '
|
|
|
5
5
|
import { expandServerConfig, getServerEnvironment } from './serverConfigEnvironment.js';
|
|
6
6
|
import { getMcpRequestGroup } from '../utils/mcpRequestGroup.js';
|
|
7
7
|
import { LegacyMcpClient } from '../clients/legacyMcpClient.js';
|
|
8
|
+
import { getElicitationCapabilities, installElicitationRelay, isElicitationClientClosed, withElicitation, } from './elicitationService.js';
|
|
8
9
|
import { LEGACY_PROTOCOL_VERSIONS } from '../utils/mcpProtocol.js';
|
|
9
10
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
10
11
|
import { createHash } from 'node:crypto';
|
|
@@ -15,7 +16,7 @@ import { applyHiddenParams, findToolConfig, removeHiddenArguments, } from '../ut
|
|
|
15
16
|
import { PrincipalRuntimeService } from './principalRuntimeService.js';
|
|
16
17
|
import { UserContextService } from './userContextService.js';
|
|
17
18
|
import { deleteCredentialBindings, missingCredentialError } from './credentialBindingService.js';
|
|
18
|
-
import { CredentialBindingError, hasCredentialTemplate, hasPersonalAuthentication, resolveCredentialArguments, validateCredentialTemplate, } from '../utils/credentialTemplate.js';
|
|
19
|
+
import { CredentialBindingError, hasCredentialTemplate, hasPersonalAuthentication, resolveCredentialArguments, hasUrlCredentialSlots, validateCredentialTemplate, } from '../utils/credentialTemplate.js';
|
|
19
20
|
import os from 'os';
|
|
20
21
|
import path from 'path';
|
|
21
22
|
import fs from 'fs';
|
|
@@ -355,12 +356,12 @@ const setSessionIsolatedClient = (sessionId, serverName, client, transport) => {
|
|
|
355
356
|
* Used when the server has `perSessionClient: true` in its config.
|
|
356
357
|
* Uses a creation lock to prevent duplicate connections from concurrent calls.
|
|
357
358
|
*/
|
|
358
|
-
const getOrCreateIsolatedClient = async (sessionId, serverInfo) => {
|
|
359
|
+
const getOrCreateIsolatedClient = async (sessionId, serverInfo, downstream) => {
|
|
359
360
|
// Quick check without lock — already exists
|
|
360
361
|
const sessionClients = sessionIsolatedClients.get(sessionId);
|
|
361
362
|
if (sessionClients) {
|
|
362
363
|
const existing = sessionClients.get(serverInfo.name);
|
|
363
|
-
if (existing) {
|
|
364
|
+
if (existing && !isElicitationClientClosed(existing.client)) {
|
|
364
365
|
return existing;
|
|
365
366
|
}
|
|
366
367
|
}
|
|
@@ -375,7 +376,7 @@ const getOrCreateIsolatedClient = async (sessionId, serverInfo) => {
|
|
|
375
376
|
const sessionClientsAfterLock = sessionIsolatedClients.get(sessionId);
|
|
376
377
|
if (sessionClientsAfterLock) {
|
|
377
378
|
const existing = sessionClientsAfterLock.get(serverInfo.name);
|
|
378
|
-
if (existing) {
|
|
379
|
+
if (existing && !isElicitationClientClosed(existing.client)) {
|
|
379
380
|
return existing;
|
|
380
381
|
}
|
|
381
382
|
}
|
|
@@ -388,7 +389,7 @@ const getOrCreateIsolatedClient = async (sessionId, serverInfo) => {
|
|
|
388
389
|
}
|
|
389
390
|
const reservedClients = sessionIsolatedClients.get(sessionId);
|
|
390
391
|
const transport = await createTransportFromConfig(serverInfo.name, serverConfig);
|
|
391
|
-
const client = createUpstreamMcpClient(serverInfo.name, () => serverInfo);
|
|
392
|
+
const client = createUpstreamMcpClient(serverInfo.name, () => serverInfo, downstream);
|
|
392
393
|
try {
|
|
393
394
|
await connectClientWithDiagnostics(client, transport, serverInfo.options || {});
|
|
394
395
|
}
|
|
@@ -840,12 +841,13 @@ const updateServerResourcesCache = (serverInfo, resources) => {
|
|
|
840
841
|
const logListChangedRefreshError = (listType) => {
|
|
841
842
|
logger.warn(`Failed to refresh ${listType} list after upstream notification`);
|
|
842
843
|
};
|
|
843
|
-
const createUpstreamMcpClient = (name, getServerInfo) => {
|
|
844
|
-
|
|
844
|
+
const createUpstreamMcpClient = (name, getServerInfo, downstream) => {
|
|
845
|
+
const elicitation = getElicitationCapabilities(downstream);
|
|
846
|
+
const client = new LegacyMcpClient({
|
|
845
847
|
name: `mcp-client-${name}`,
|
|
846
848
|
version: '1.0.0',
|
|
847
849
|
}, {
|
|
848
|
-
capabilities: MCP_APPS_CAPABILITIES,
|
|
850
|
+
capabilities: { ...MCP_APPS_CAPABILITIES, ...(elicitation ? { elicitation } : {}) },
|
|
849
851
|
jsonSchemaValidator: new ResilientJsonSchemaValidator(),
|
|
850
852
|
listChanged: {
|
|
851
853
|
tools: {
|
|
@@ -895,6 +897,9 @@ const createUpstreamMcpClient = (name, getServerInfo) => {
|
|
|
895
897
|
},
|
|
896
898
|
},
|
|
897
899
|
});
|
|
900
|
+
if (elicitation && downstream)
|
|
901
|
+
installElicitationRelay(client, downstream);
|
|
902
|
+
return client;
|
|
898
903
|
};
|
|
899
904
|
// Normalize and infer server type for safe client display
|
|
900
905
|
const normalizeServerType = (type) => {
|
|
@@ -1229,7 +1234,7 @@ export const createTransportFromConfig = async (name, conf) => {
|
|
|
1229
1234
|
// skip the internal-IP blocklist. allowInternal also governs per-hop
|
|
1230
1235
|
// redirect validation in createRedirectValidatingFetch below.
|
|
1231
1236
|
const ownerUser = conf.owner ? await getUserDao().findByUsername(conf.owner) : null;
|
|
1232
|
-
const allowInternal = !!ownerUser?.isAdmin;
|
|
1237
|
+
const allowInternal = !!ownerUser?.isAdmin && !hasUrlCredentialSlots(conf);
|
|
1233
1238
|
if (conf.url) {
|
|
1234
1239
|
await assertSafeUrl(conf.url, { allowInternal });
|
|
1235
1240
|
}
|
|
@@ -1370,7 +1375,7 @@ const withoutHiddenArguments = (serverInfo, toolName, args) => {
|
|
|
1370
1375
|
return removeHiddenArguments(args, tool?.inputSchema, toolConfig);
|
|
1371
1376
|
};
|
|
1372
1377
|
// Helper function to handle client.callTool with reconnection logic
|
|
1373
|
-
const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries = 1, isolated) => {
|
|
1378
|
+
const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries = 1, isolated, extra) => {
|
|
1374
1379
|
// Local, reassignable refs so a reconnect can swap in the new connection for
|
|
1375
1380
|
// the next retry attempt. For isolated calls these track the per-session
|
|
1376
1381
|
// client/transport; otherwise they mirror the shared serverInfo connection.
|
|
@@ -1389,12 +1394,23 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
|
|
|
1389
1394
|
}
|
|
1390
1395
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1391
1396
|
try {
|
|
1392
|
-
const
|
|
1393
|
-
|
|
1394
|
-
|
|
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
|
+
});
|
|
1395
1403
|
return result;
|
|
1396
1404
|
}
|
|
1397
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;
|
|
1398
1414
|
const isHttp40xError = isRecoverableHttp4xxError(error);
|
|
1399
1415
|
// Only retry for StreamableHTTPClientTransport and SSE transports
|
|
1400
1416
|
const isStreamableHttp = transport instanceof StreamableHTTPClientTransport;
|
|
@@ -2940,24 +2956,25 @@ const getMcpAppsRouteContext = async (sessionId, group) => {
|
|
|
2940
2956
|
return eligible.length > 0 ? { enabled: true, serverInfos: eligible } : { enabled: false };
|
|
2941
2957
|
}
|
|
2942
2958
|
const { filteredServerInfos, serverConfigsByName } = await getFilteredServerInfosForGroup(group);
|
|
2943
|
-
const
|
|
2944
|
-
|
|
2959
|
+
const eligibleServerInfos = filteredServerInfos.filter((serverInfo) => canServeToolRequests(serverInfo) &&
|
|
2960
|
+
(!!serverInfo.client || serverInfo.config?.startOnDemand === true));
|
|
2961
|
+
if (eligibleServerInfos.length === 0) {
|
|
2945
2962
|
return { enabled: false };
|
|
2946
2963
|
}
|
|
2947
2964
|
// Connection availability must not turn an aggregate route into a raw-name
|
|
2948
2965
|
// route: OpenAPI uses openApiClient, and tool calls acquire only the target
|
|
2949
2966
|
// personal runtime. Match resolveToolInGroup's configured-route ambiguity check.
|
|
2950
|
-
if (
|
|
2967
|
+
if (eligibleServerInfos.length === 1 &&
|
|
2951
2968
|
filteredServerInfos.length === 1 &&
|
|
2952
2969
|
serverConfigsByName.size <= 1) {
|
|
2953
2970
|
return {
|
|
2954
2971
|
enabled: true,
|
|
2955
|
-
serverInfo:
|
|
2972
|
+
serverInfo: eligibleServerInfos[0],
|
|
2956
2973
|
};
|
|
2957
2974
|
}
|
|
2958
2975
|
return {
|
|
2959
2976
|
enabled: true,
|
|
2960
|
-
serverInfos:
|
|
2977
|
+
serverInfos: eligibleServerInfos,
|
|
2961
2978
|
};
|
|
2962
2979
|
};
|
|
2963
2980
|
const normalizeToolNameForServer = (serverName, toolName) => {
|
|
@@ -2992,9 +3009,14 @@ const projectNameForGroup = (name, serverName, serverConfig) => {
|
|
|
2992
3009
|
const resolveNameFromGroup = (name, serverName, serverConfig) => {
|
|
2993
3010
|
return replacePrefixedServerName(name, getExposedServerName(serverName, serverConfig), serverName);
|
|
2994
3011
|
};
|
|
2995
|
-
const findToolOnServer = (serverInfo, toolName, allowRawName) => {
|
|
2996
|
-
|
|
2997
|
-
|
|
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));
|
|
2998
3020
|
if (!tool) {
|
|
2999
3021
|
return undefined;
|
|
3000
3022
|
}
|
|
@@ -3043,7 +3065,7 @@ const resolveToolInGroup = async (group, toolName) => {
|
|
|
3043
3065
|
}
|
|
3044
3066
|
const serverConfig = serverConfigsByName.get(serverInfo.name);
|
|
3045
3067
|
const internalToolName = resolveNameFromGroup(toolName, serverInfo.name, serverConfig);
|
|
3046
|
-
const tool = findToolOnServer(serverInfo, internalToolName, allowRawName);
|
|
3068
|
+
const tool = findToolOnServer(serverInfo, internalToolName, allowRawName, serverConfigsByName.size === 0);
|
|
3047
3069
|
if (!tool) {
|
|
3048
3070
|
continue;
|
|
3049
3071
|
}
|
|
@@ -3055,6 +3077,17 @@ const resolveToolInGroup = async (group, toolName) => {
|
|
|
3055
3077
|
}
|
|
3056
3078
|
return undefined;
|
|
3057
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
|
+
};
|
|
3058
3091
|
async function resolvePromptInGroup(group, promptName) {
|
|
3059
3092
|
const lookupGroup = getGroupLookupName(group);
|
|
3060
3093
|
if (!lookupGroup) {
|
|
@@ -3067,7 +3100,7 @@ async function resolvePromptInGroup(group, promptName) {
|
|
|
3067
3100
|
}
|
|
3068
3101
|
const serverConfig = serverConfigsByName.get(serverInfo.name);
|
|
3069
3102
|
const internalPromptName = resolveNameFromGroup(promptName, serverInfo.name, serverConfig);
|
|
3070
|
-
const prompt = serverInfo
|
|
3103
|
+
const prompt = await findEnabledPromptOnServer(serverInfo, internalPromptName);
|
|
3071
3104
|
if (!prompt) {
|
|
3072
3105
|
continue;
|
|
3073
3106
|
}
|
|
@@ -3079,21 +3112,19 @@ async function resolvePromptInGroup(group, promptName) {
|
|
|
3079
3112
|
}
|
|
3080
3113
|
return undefined;
|
|
3081
3114
|
}
|
|
3082
|
-
const projectToolForDownstream = (serverName, tool, appsRouteContext, serverConfig) => {
|
|
3115
|
+
const projectToolForDownstream = (serverName, tool, appsRouteContext, serverConfig, useRawName = false) => {
|
|
3083
3116
|
if (!appsRouteContext.enabled && isAppOnlyTool(tool)) {
|
|
3084
3117
|
return undefined;
|
|
3085
3118
|
}
|
|
3086
3119
|
const projectedTool = appsRouteContext.enabled ? tool : stripMcpAppsMetadata(tool);
|
|
3087
|
-
//
|
|
3088
|
-
|
|
3089
|
-
if (appsRouteContext.serverInfo) {
|
|
3120
|
+
// Naming follows the route, independently of Apps capabilities or connection state.
|
|
3121
|
+
if (useRawName) {
|
|
3090
3122
|
return {
|
|
3091
3123
|
...projectedTool,
|
|
3092
3124
|
name: normalizeToolNameForServer(serverName, projectedTool.name),
|
|
3093
3125
|
};
|
|
3094
3126
|
}
|
|
3095
|
-
//
|
|
3096
|
-
// name so it can still be routed to the right server unambiguously.
|
|
3127
|
+
// Group and aggregate routes keep qualified names, including single-member groups.
|
|
3097
3128
|
return {
|
|
3098
3129
|
...projectedTool,
|
|
3099
3130
|
name: projectNameForGroup(projectedTool.name, serverName, serverConfig),
|
|
@@ -3106,6 +3137,7 @@ const listGroupTools = async (group, filteredServerInfos, serverConfigsByName, a
|
|
|
3106
3137
|
await Promise.allSettled(filteredServerInfos
|
|
3107
3138
|
.filter((si) => si.config?.startOnDemand === true && si.tools.length === 0 && si.spawningPromise)
|
|
3108
3139
|
.map((si) => si.spawningPromise));
|
|
3140
|
+
const useRawName = !!group && !isSmartRoutingGroup(group) && !(await findRouteGroup(group));
|
|
3109
3141
|
const allTools = [];
|
|
3110
3142
|
for (const serverInfo of filteredServerInfos) {
|
|
3111
3143
|
if (serverInfo.tools && serverInfo.tools.length > 0) {
|
|
@@ -3125,7 +3157,7 @@ const listGroupTools = async (group, filteredServerInfos, serverConfigsByName, a
|
|
|
3125
3157
|
};
|
|
3126
3158
|
});
|
|
3127
3159
|
allTools.push(...toolsWithCustomDescriptions.flatMap((tool) => {
|
|
3128
|
-
const projectedTool = projectToolForDownstream(serverInfo.name, tool, appsRouteContext, groupServerConfig);
|
|
3160
|
+
const projectedTool = projectToolForDownstream(serverInfo.name, tool, appsRouteContext, groupServerConfig, useRawName);
|
|
3129
3161
|
return projectedTool ? [projectedTool] : [];
|
|
3130
3162
|
}));
|
|
3131
3163
|
}
|
|
@@ -3282,28 +3314,33 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3282
3314
|
};
|
|
3283
3315
|
try {
|
|
3284
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);
|
|
3285
3324
|
// Special handling for smart routing tools
|
|
3286
|
-
if (request.params.name === 'search_tools') {
|
|
3325
|
+
if (!upstreamOwnsMetaName && request.params.name === 'search_tools') {
|
|
3287
3326
|
const { query, limit = 10 } = request.params.arguments || {};
|
|
3288
3327
|
return await handleSearchToolsRequest(query, limit, sessionId, group);
|
|
3289
3328
|
}
|
|
3290
3329
|
// Special handling for describe_tool (progressive disclosure mode)
|
|
3291
|
-
if (request.params.name === 'describe_tool') {
|
|
3330
|
+
if (!upstreamOwnsMetaName && request.params.name === 'describe_tool') {
|
|
3292
3331
|
const { toolName } = request.params.arguments || {};
|
|
3293
3332
|
return await handleDescribeToolRequest(toolName, sessionId, group);
|
|
3294
3333
|
}
|
|
3295
3334
|
// Special handling for call_tool
|
|
3296
|
-
if (request.params.name === 'call_tool') {
|
|
3335
|
+
if (!upstreamOwnsMetaName && request.params.name === 'call_tool') {
|
|
3297
3336
|
const { toolName } = request.params.arguments || {};
|
|
3298
3337
|
if (!toolName) {
|
|
3299
3338
|
throw new Error('toolName parameter is required');
|
|
3300
3339
|
}
|
|
3301
3340
|
const { arguments: toolArgs } = request.params.arguments || {};
|
|
3302
|
-
//
|
|
3303
|
-
//
|
|
3304
|
-
|
|
3305
|
-
// resolution below, same as an ordinary (non-Apps) group route.
|
|
3306
|
-
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;
|
|
3307
3344
|
let targetServerInfo;
|
|
3308
3345
|
let targetToolName = toolName;
|
|
3309
3346
|
let targetTool;
|
|
@@ -3364,7 +3401,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3364
3401
|
arguments: summarizeArgumentsForLogging(finalArgs),
|
|
3365
3402
|
});
|
|
3366
3403
|
// Remove server prefix from tool name if present
|
|
3367
|
-
const cleanToolName = normalizeToolNameForServer(targetServerInfo.name,
|
|
3404
|
+
const cleanToolName = normalizeToolNameForServer(targetServerInfo.name, tool.name);
|
|
3368
3405
|
// Extract passthrough headers from extra or request context
|
|
3369
3406
|
let passthroughHeaders;
|
|
3370
3407
|
let requestHeaders = null;
|
|
@@ -3447,7 +3484,9 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3447
3484
|
if (!stateId) {
|
|
3448
3485
|
throw new Error(`Server '${targetServerInfo.name}' requires per-session client isolation and is not yet available on stateless MCP requests`);
|
|
3449
3486
|
}
|
|
3450
|
-
const isolated = await getOrCreateIsolatedClient(stateId, targetServerInfo)
|
|
3487
|
+
const isolated = await getOrCreateIsolatedClient(stateId, targetServerInfo, extra.mcpReq && !requestContextService.getRequestContext()?.stateless
|
|
3488
|
+
? servers[sessionId]
|
|
3489
|
+
: undefined);
|
|
3451
3490
|
isolatedCtx = {
|
|
3452
3491
|
sessionId: stateId,
|
|
3453
3492
|
client: isolated.client,
|
|
@@ -3465,12 +3504,12 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3465
3504
|
arguments: summarizeArgumentsForLogging(finalArgs),
|
|
3466
3505
|
perSessionClient: !!targetServerInfo.config?.perSessionClient,
|
|
3467
3506
|
});
|
|
3468
|
-
const cleanToolName = normalizeToolNameForServer(targetServerInfo.name,
|
|
3507
|
+
const cleanToolName = normalizeToolNameForServer(targetServerInfo.name, tool.name);
|
|
3469
3508
|
await reserveHostedIfNeeded(targetServerInfo.name, cleanToolName);
|
|
3470
3509
|
const result = await callToolWithReconnect(targetServerInfo, {
|
|
3471
3510
|
name: cleanToolName,
|
|
3472
3511
|
arguments: finalArgs,
|
|
3473
|
-
}, targetServerInfo.options || {}, 1, isolatedCtx);
|
|
3512
|
+
}, targetServerInfo.options || {}, 1, isolatedCtx, extra);
|
|
3474
3513
|
await settleHostedIfNeeded({
|
|
3475
3514
|
success: !result.isError,
|
|
3476
3515
|
requestContent: finalArgs,
|
|
@@ -3506,13 +3545,9 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3506
3545
|
}
|
|
3507
3546
|
// Regular tool handling
|
|
3508
3547
|
const lookupGroup = getGroupLookupName(group);
|
|
3509
|
-
//
|
|
3510
|
-
//
|
|
3511
|
-
|
|
3512
|
-
// group lookup below, same as an ordinary (non-Apps) group route,
|
|
3513
|
-
// just without stripping Apps metadata or hiding app-only tools
|
|
3514
|
-
// (assertToolAvailableForRoute below still gates on appsRouteContext.enabled).
|
|
3515
|
-
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;
|
|
3516
3551
|
const groupTool = !singleServerAppsRoute && lookupGroup
|
|
3517
3552
|
? await resolveToolInGroup(group, request.params.name)
|
|
3518
3553
|
: undefined;
|
|
@@ -3540,7 +3575,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3540
3575
|
// For OpenAPI servers, use the OpenAPI client
|
|
3541
3576
|
const openApiClient = serverInfo.openApiClient;
|
|
3542
3577
|
// Remove server prefix from tool name if present
|
|
3543
|
-
const cleanToolName = normalizeToolNameForServer(serverInfo.name,
|
|
3578
|
+
const cleanToolName = normalizeToolNameForServer(serverInfo.name, tool.name);
|
|
3544
3579
|
logger.log('Invoking OpenAPI tool', {
|
|
3545
3580
|
toolName: cleanToolName,
|
|
3546
3581
|
serverName: serverInfo.name,
|
|
@@ -3628,15 +3663,17 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3628
3663
|
if (!stateId) {
|
|
3629
3664
|
throw new Error(`Server '${serverInfo.name}' requires per-session client isolation and is not yet available on stateless MCP requests`);
|
|
3630
3665
|
}
|
|
3631
|
-
const isolated = await getOrCreateIsolatedClient(stateId, serverInfo)
|
|
3666
|
+
const isolated = await getOrCreateIsolatedClient(stateId, serverInfo, extra.mcpReq && !requestContextService.getRequestContext()?.stateless
|
|
3667
|
+
? servers[sessionId]
|
|
3668
|
+
: undefined);
|
|
3632
3669
|
isolatedCtx = { sessionId: stateId, client: isolated.client, transport: isolated.transport };
|
|
3633
3670
|
}
|
|
3634
3671
|
else if (!serverInfo.client) {
|
|
3635
3672
|
throw new Error(`Client not found for server: ${serverInfo.name}`);
|
|
3636
3673
|
}
|
|
3637
|
-
const cleanToolName = normalizeToolNameForServer(serverInfo.name,
|
|
3674
|
+
const cleanToolName = normalizeToolNameForServer(serverInfo.name, tool.name);
|
|
3638
3675
|
await reserveHostedIfNeeded(serverInfo.name, cleanToolName);
|
|
3639
|
-
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);
|
|
3640
3677
|
await settleHostedIfNeeded({
|
|
3641
3678
|
success: !result.isError,
|
|
3642
3679
|
requestContent: request.params.arguments,
|
|
@@ -3735,7 +3772,7 @@ const handleGetPromptRequestImpl = async (request, extra) => {
|
|
|
3735
3772
|
const { name, arguments: promptArgs } = request.params;
|
|
3736
3773
|
const group = getMcpRequestGroup(extra);
|
|
3737
3774
|
// Check built-in prompts first
|
|
3738
|
-
const builtinPrompt = await getBuiltinPromptDao().findByName(name);
|
|
3775
|
+
const builtinPrompt = !extra?.server && (await getBuiltinPromptDao().findByName(name));
|
|
3739
3776
|
if (builtinPrompt && builtinPrompt.enabled !== false) {
|
|
3740
3777
|
// Perform {{param}} template substitution
|
|
3741
3778
|
let content = builtinPrompt.template;
|
|
@@ -3757,7 +3794,12 @@ const handleGetPromptRequestImpl = async (request, extra) => {
|
|
|
3757
3794
|
let promptNameForServer = name;
|
|
3758
3795
|
const lookupGroup = getGroupLookupName(group);
|
|
3759
3796
|
if (extra && extra.server) {
|
|
3760
|
-
|
|
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
|
+
}
|
|
3761
3803
|
}
|
|
3762
3804
|
else if (lookupGroup) {
|
|
3763
3805
|
const groupPrompt = await resolvePromptInGroup(lookupGroup, name);
|
|
@@ -3767,10 +3809,14 @@ const handleGetPromptRequestImpl = async (request, extra) => {
|
|
|
3767
3809
|
}
|
|
3768
3810
|
}
|
|
3769
3811
|
else {
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
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
|
+
}
|
|
3774
3820
|
}
|
|
3775
3821
|
if (!server) {
|
|
3776
3822
|
throw new ToolUnavailableError(`Prompt not available: ${name}`, classifyUnavailableReason(name));
|
|
@@ -3839,7 +3885,7 @@ const handleListPromptsRequestImpl = async (_, extra) => {
|
|
|
3839
3885
|
let enabledPrompts = serverInfo.prompts;
|
|
3840
3886
|
if (serverConfig && serverConfig.prompts) {
|
|
3841
3887
|
enabledPrompts = serverInfo.prompts.filter((prompt) => {
|
|
3842
|
-
const promptConfig = serverConfig.prompts
|
|
3888
|
+
const promptConfig = findToolConfig(serverConfig.prompts, serverInfo.name, prompt.name, serverInfo.prompts.map((item) => item.name));
|
|
3843
3889
|
// If prompt is not in config, it's enabled by default
|
|
3844
3890
|
return promptConfig?.enabled !== false;
|
|
3845
3891
|
});
|
|
@@ -4006,6 +4052,12 @@ const handleReadResourceRequestImpl = async (request, extra) => {
|
|
|
4006
4052
|
? [appsRouteContext.serverInfo]
|
|
4007
4053
|
: (appsRouteContext.serverInfos ?? []);
|
|
4008
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;
|
|
4009
4061
|
// An on-demand candidate may be idle yet still advertise its cached widget
|
|
4010
4062
|
// link, so a host can read the ui:// resource before any tool call has
|
|
4011
4063
|
// woken it. Hold off its idle shutdown for every read, awake or not, so an
|
|
@@ -4201,7 +4253,7 @@ const withPrincipalServers = (handler, operation) => async (request, extra) => {
|
|
|
4201
4253
|
if (!serverInfos.some((info) => hasPersonalAuthentication(info.config)))
|
|
4202
4254
|
return handler(request, extra);
|
|
4203
4255
|
// Built-in content does not require an upstream credential.
|
|
4204
|
-
if (operation === 'prompt') {
|
|
4256
|
+
if (operation === 'prompt' && !extra?.server) {
|
|
4205
4257
|
const builtin = await getBuiltinPromptDao().findByName(request.params?.name);
|
|
4206
4258
|
if (builtin && builtin.enabled !== false)
|
|
4207
4259
|
return handler(request, extra);
|