@samanhappy/mcphub 1.1.8 → 1.1.10
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 +117 -62
- 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-DDa3yAck.js} +2 -2
- package/frontend/dist/assets/{ActivityPage-CJdAfOUF.js.map → ActivityPage-DDa3yAck.js.map} +1 -1
- package/frontend/dist/assets/{CopyClientConfigDialog-DKktm8jj.js → CopyClientConfigDialog-DPQOggV-.js} +2 -2
- package/frontend/dist/assets/{CopyClientConfigDialog-DKktm8jj.js.map → CopyClientConfigDialog-DPQOggV-.js.map} +1 -1
- package/frontend/dist/assets/CredentialsPage-DmxY-iXb.js +2 -0
- package/frontend/dist/assets/CredentialsPage-DmxY-iXb.js.map +1 -0
- package/frontend/dist/assets/{Dashboard-DM_IiBIT.js → Dashboard-DCvveQBj.js} +2 -2
- package/frontend/dist/assets/{Dashboard-DM_IiBIT.js.map → Dashboard-DCvveQBj.js.map} +1 -1
- package/frontend/dist/assets/{EndpointCopy-DHQjJesJ.js → EndpointCopy-DRWz8guH.js} +2 -2
- package/frontend/dist/assets/{EndpointCopy-DHQjJesJ.js.map → EndpointCopy-DRWz8guH.js.map} +1 -1
- package/frontend/dist/assets/GroupsPage-DtoTWqWU.js +33 -0
- package/frontend/dist/assets/GroupsPage-DtoTWqWU.js.map +1 -0
- package/frontend/dist/assets/{LoginPage-CKrI0By4.js → LoginPage-BVBf8nr6.js} +2 -2
- package/frontend/dist/assets/{LoginPage-CKrI0By4.js.map → LoginPage-BVBf8nr6.js.map} +1 -1
- package/frontend/dist/assets/{LogsPage-Dh8gRpEH.js → LogsPage-DbsmNJcA.js} +2 -2
- package/frontend/dist/assets/{LogsPage-Dh8gRpEH.js.map → LogsPage-DbsmNJcA.js.map} +1 -1
- package/frontend/dist/assets/{MarketPage-BN4qL8L9.js → MarketPage-BOggDLJb.js} +2 -2
- package/frontend/dist/assets/{MarketPage-BN4qL8L9.js.map → MarketPage-BOggDLJb.js.map} +1 -1
- package/frontend/dist/assets/{OAuthConsentPage-Cyj-rrA4.js → OAuthConsentPage-VON__2op.js} +2 -2
- package/frontend/dist/assets/{OAuthConsentPage-Cyj-rrA4.js.map → OAuthConsentPage-VON__2op.js.map} +1 -1
- package/frontend/dist/assets/{PromptsPage-YWD04XeI.js → PromptsPage-BNkvIDZU.js} +2 -2
- package/frontend/dist/assets/{PromptsPage-YWD04XeI.js.map → PromptsPage-BNkvIDZU.js.map} +1 -1
- package/frontend/dist/assets/{ResourcesPage-CVRzHCf4.js → ResourcesPage-4WBvamN_.js} +2 -2
- package/frontend/dist/assets/{ResourcesPage-CVRzHCf4.js.map → ResourcesPage-4WBvamN_.js.map} +1 -1
- package/frontend/dist/assets/{ServersPage-DJBurfOe.js → ServersPage-C7000Szj.js} +7 -7
- package/frontend/dist/assets/ServersPage-C7000Szj.js.map +1 -0
- package/frontend/dist/assets/{SettingsPage-3sK6NJRn.js → SettingsPage-YX6dJjrM.js} +2 -2
- package/frontend/dist/assets/{SettingsPage-3sK6NJRn.js.map → SettingsPage-YX6dJjrM.js.map} +1 -1
- package/frontend/dist/assets/{StatusDot-CLZidCyq.js → StatusDot-mgJveZ2h.js} +2 -2
- package/frontend/dist/assets/{StatusDot-CLZidCyq.js.map → StatusDot-mgJveZ2h.js.map} +1 -1
- package/frontend/dist/assets/{ToggleGroup-Dfh4jKpP.js → ToggleGroup-DXyuwPy7.js} +2 -2
- package/frontend/dist/assets/{ToggleGroup-Dfh4jKpP.js.map → ToggleGroup-DXyuwPy7.js.map} +1 -1
- package/frontend/dist/assets/{UsersPage-DfFqSRVb.js → UsersPage-BiUn8c0z.js} +2 -2
- package/frontend/dist/assets/{UsersPage-DfFqSRVb.js.map → UsersPage-BiUn8c0z.js.map} +1 -1
- package/frontend/dist/assets/{clipboard-wUZFI68F.js → clipboard-7MOQ-4Cg.js} +2 -2
- package/frontend/dist/assets/{clipboard-wUZFI68F.js.map → clipboard-7MOQ-4Cg.js.map} +1 -1
- package/frontend/dist/assets/index-B3hkLjCv.css +1 -0
- package/frontend/dist/assets/index-BKFFbupY.js +3 -0
- package/frontend/dist/assets/index-BKFFbupY.js.map +1 -0
- package/frontend/dist/assets/{resourceService-D7yitP7i.js → resourceService-BOn21QbP.js} +2 -2
- package/frontend/dist/assets/{resourceService-D7yitP7i.js.map → resourceService-BOn21QbP.js.map} +1 -1
- package/frontend/dist/assets/{useCostData-33G6s4EW.js → useCostData-3iIVsNH6.js} +2 -2
- package/frontend/dist/assets/{useCostData-33G6s4EW.js.map → useCostData-3iIVsNH6.js.map} +1 -1
- package/frontend/dist/assets/variableDetection-Cpa0rIrf.js +18 -0
- package/frontend/dist/assets/variableDetection-Cpa0rIrf.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/ServersPage-DJBurfOe.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;
|
|
@@ -2107,7 +2123,7 @@ export const getServersInfo = async (page, limit, user) => {
|
|
|
2107
2123
|
}));
|
|
2108
2124
|
const infos = presentedInfos
|
|
2109
2125
|
.filter((info) => requestedServerNames.has(info.name)) // Only include requested servers
|
|
2110
|
-
.map(({ name, version, packageVersion, latestVersion, updateAvailable, instructions, owner, visibility, status, tools, prompts, resources, createTime, error, oauth, }) => {
|
|
2126
|
+
.map(({ name, version, client, packageVersion, latestVersion, updateAvailable, instructions, owner, visibility, status, tools, prompts, resources, createTime, error, oauth, }) => {
|
|
2111
2127
|
const serverConfig = allServers.find((server) => server.name === name);
|
|
2112
2128
|
const enabled = serverConfig ? serverConfig.enabled !== false : true;
|
|
2113
2129
|
const resolvedType = inferServerType(serverConfig);
|
|
@@ -2138,6 +2154,9 @@ export const getServersInfo = async (page, limit, user) => {
|
|
|
2138
2154
|
return {
|
|
2139
2155
|
name,
|
|
2140
2156
|
version,
|
|
2157
|
+
protocolVersion: enabled && status === 'connected' && resolvedType !== 'openapi'
|
|
2158
|
+
? client?.getNegotiatedProtocolVersion?.()
|
|
2159
|
+
: undefined,
|
|
2141
2160
|
instructions,
|
|
2142
2161
|
owner,
|
|
2143
2162
|
visibility,
|
|
@@ -2940,24 +2959,25 @@ const getMcpAppsRouteContext = async (sessionId, group) => {
|
|
|
2940
2959
|
return eligible.length > 0 ? { enabled: true, serverInfos: eligible } : { enabled: false };
|
|
2941
2960
|
}
|
|
2942
2961
|
const { filteredServerInfos, serverConfigsByName } = await getFilteredServerInfosForGroup(group);
|
|
2943
|
-
const
|
|
2944
|
-
|
|
2962
|
+
const eligibleServerInfos = filteredServerInfos.filter((serverInfo) => canServeToolRequests(serverInfo) &&
|
|
2963
|
+
(!!serverInfo.client || serverInfo.config?.startOnDemand === true));
|
|
2964
|
+
if (eligibleServerInfos.length === 0) {
|
|
2945
2965
|
return { enabled: false };
|
|
2946
2966
|
}
|
|
2947
2967
|
// Connection availability must not turn an aggregate route into a raw-name
|
|
2948
2968
|
// route: OpenAPI uses openApiClient, and tool calls acquire only the target
|
|
2949
2969
|
// personal runtime. Match resolveToolInGroup's configured-route ambiguity check.
|
|
2950
|
-
if (
|
|
2970
|
+
if (eligibleServerInfos.length === 1 &&
|
|
2951
2971
|
filteredServerInfos.length === 1 &&
|
|
2952
2972
|
serverConfigsByName.size <= 1) {
|
|
2953
2973
|
return {
|
|
2954
2974
|
enabled: true,
|
|
2955
|
-
serverInfo:
|
|
2975
|
+
serverInfo: eligibleServerInfos[0],
|
|
2956
2976
|
};
|
|
2957
2977
|
}
|
|
2958
2978
|
return {
|
|
2959
2979
|
enabled: true,
|
|
2960
|
-
serverInfos:
|
|
2980
|
+
serverInfos: eligibleServerInfos,
|
|
2961
2981
|
};
|
|
2962
2982
|
};
|
|
2963
2983
|
const normalizeToolNameForServer = (serverName, toolName) => {
|
|
@@ -2992,9 +3012,14 @@ const projectNameForGroup = (name, serverName, serverConfig) => {
|
|
|
2992
3012
|
const resolveNameFromGroup = (name, serverName, serverConfig) => {
|
|
2993
3013
|
return replacePrefixedServerName(name, getExposedServerName(serverName, serverConfig), serverName);
|
|
2994
3014
|
};
|
|
2995
|
-
const findToolOnServer = (serverInfo, toolName, allowRawName) => {
|
|
2996
|
-
|
|
2997
|
-
|
|
3015
|
+
const findToolOnServer = (serverInfo, toolName, allowRawName, preferRawName = false) => {
|
|
3016
|
+
// A listed upstream name takes precedence over a colliding legacy cache alias.
|
|
3017
|
+
const rawTool = preferRawName && allowRawName
|
|
3018
|
+
? serverInfo.tools.find((tool) => normalizeToolNameForServer(serverInfo.name, tool.name) === toolName)
|
|
3019
|
+
: undefined;
|
|
3020
|
+
const tool = rawTool ||
|
|
3021
|
+
serverInfo.tools.find((tool) => tool.name === toolName ||
|
|
3022
|
+
(allowRawName && normalizeToolNameForServer(serverInfo.name, tool.name) === toolName));
|
|
2998
3023
|
if (!tool) {
|
|
2999
3024
|
return undefined;
|
|
3000
3025
|
}
|
|
@@ -3043,7 +3068,7 @@ const resolveToolInGroup = async (group, toolName) => {
|
|
|
3043
3068
|
}
|
|
3044
3069
|
const serverConfig = serverConfigsByName.get(serverInfo.name);
|
|
3045
3070
|
const internalToolName = resolveNameFromGroup(toolName, serverInfo.name, serverConfig);
|
|
3046
|
-
const tool = findToolOnServer(serverInfo, internalToolName, allowRawName);
|
|
3071
|
+
const tool = findToolOnServer(serverInfo, internalToolName, allowRawName, serverConfigsByName.size === 0);
|
|
3047
3072
|
if (!tool) {
|
|
3048
3073
|
continue;
|
|
3049
3074
|
}
|
|
@@ -3055,6 +3080,17 @@ const resolveToolInGroup = async (group, toolName) => {
|
|
|
3055
3080
|
}
|
|
3056
3081
|
return undefined;
|
|
3057
3082
|
};
|
|
3083
|
+
const findEnabledPromptOnServer = async (serverInfo, name, allowRawName = false) => {
|
|
3084
|
+
if (serverInfo.status !== 'connected' || serverInfo.enabled === false)
|
|
3085
|
+
return undefined;
|
|
3086
|
+
const prompt = serverInfo.prompts.find((item) => item.name === name ||
|
|
3087
|
+
(allowRawName && item.name === `${serverInfo.name}${getNameSeparator()}${name}`));
|
|
3088
|
+
if (!prompt)
|
|
3089
|
+
return undefined;
|
|
3090
|
+
const config = await getServerDao().findById(serverInfo.name);
|
|
3091
|
+
const promptConfig = findToolConfig(config?.prompts, serverInfo.name, prompt.name, serverInfo.prompts.map((item) => item.name));
|
|
3092
|
+
return config?.enabled === false || promptConfig?.enabled === false ? undefined : prompt;
|
|
3093
|
+
};
|
|
3058
3094
|
async function resolvePromptInGroup(group, promptName) {
|
|
3059
3095
|
const lookupGroup = getGroupLookupName(group);
|
|
3060
3096
|
if (!lookupGroup) {
|
|
@@ -3067,7 +3103,7 @@ async function resolvePromptInGroup(group, promptName) {
|
|
|
3067
3103
|
}
|
|
3068
3104
|
const serverConfig = serverConfigsByName.get(serverInfo.name);
|
|
3069
3105
|
const internalPromptName = resolveNameFromGroup(promptName, serverInfo.name, serverConfig);
|
|
3070
|
-
const prompt = serverInfo
|
|
3106
|
+
const prompt = await findEnabledPromptOnServer(serverInfo, internalPromptName);
|
|
3071
3107
|
if (!prompt) {
|
|
3072
3108
|
continue;
|
|
3073
3109
|
}
|
|
@@ -3079,21 +3115,19 @@ async function resolvePromptInGroup(group, promptName) {
|
|
|
3079
3115
|
}
|
|
3080
3116
|
return undefined;
|
|
3081
3117
|
}
|
|
3082
|
-
const projectToolForDownstream = (serverName, tool, appsRouteContext, serverConfig) => {
|
|
3118
|
+
const projectToolForDownstream = (serverName, tool, appsRouteContext, serverConfig, useRawName = false) => {
|
|
3083
3119
|
if (!appsRouteContext.enabled && isAppOnlyTool(tool)) {
|
|
3084
3120
|
return undefined;
|
|
3085
3121
|
}
|
|
3086
3122
|
const projectedTool = appsRouteContext.enabled ? tool : stripMcpAppsMetadata(tool);
|
|
3087
|
-
//
|
|
3088
|
-
|
|
3089
|
-
if (appsRouteContext.serverInfo) {
|
|
3123
|
+
// Naming follows the route, independently of Apps capabilities or connection state.
|
|
3124
|
+
if (useRawName) {
|
|
3090
3125
|
return {
|
|
3091
3126
|
...projectedTool,
|
|
3092
3127
|
name: normalizeToolNameForServer(serverName, projectedTool.name),
|
|
3093
3128
|
};
|
|
3094
3129
|
}
|
|
3095
|
-
//
|
|
3096
|
-
// name so it can still be routed to the right server unambiguously.
|
|
3130
|
+
// Group and aggregate routes keep qualified names, including single-member groups.
|
|
3097
3131
|
return {
|
|
3098
3132
|
...projectedTool,
|
|
3099
3133
|
name: projectNameForGroup(projectedTool.name, serverName, serverConfig),
|
|
@@ -3106,6 +3140,7 @@ const listGroupTools = async (group, filteredServerInfos, serverConfigsByName, a
|
|
|
3106
3140
|
await Promise.allSettled(filteredServerInfos
|
|
3107
3141
|
.filter((si) => si.config?.startOnDemand === true && si.tools.length === 0 && si.spawningPromise)
|
|
3108
3142
|
.map((si) => si.spawningPromise));
|
|
3143
|
+
const useRawName = !!group && !isSmartRoutingGroup(group) && !(await findRouteGroup(group));
|
|
3109
3144
|
const allTools = [];
|
|
3110
3145
|
for (const serverInfo of filteredServerInfos) {
|
|
3111
3146
|
if (serverInfo.tools && serverInfo.tools.length > 0) {
|
|
@@ -3125,7 +3160,7 @@ const listGroupTools = async (group, filteredServerInfos, serverConfigsByName, a
|
|
|
3125
3160
|
};
|
|
3126
3161
|
});
|
|
3127
3162
|
allTools.push(...toolsWithCustomDescriptions.flatMap((tool) => {
|
|
3128
|
-
const projectedTool = projectToolForDownstream(serverInfo.name, tool, appsRouteContext, groupServerConfig);
|
|
3163
|
+
const projectedTool = projectToolForDownstream(serverInfo.name, tool, appsRouteContext, groupServerConfig, useRawName);
|
|
3129
3164
|
return projectedTool ? [projectedTool] : [];
|
|
3130
3165
|
}));
|
|
3131
3166
|
}
|
|
@@ -3282,28 +3317,33 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3282
3317
|
};
|
|
3283
3318
|
try {
|
|
3284
3319
|
appsRouteContext = await getMcpAppsRouteContext(sessionId, group);
|
|
3320
|
+
const lookupRoute = getGroupLookupName(group);
|
|
3321
|
+
// Direct routes own their listed raw names, even when a tool is disabled.
|
|
3322
|
+
const upstreamOwnsMetaName = SMART_ROUTING_META_TOOL_NAMES.has(request.params.name) &&
|
|
3323
|
+
!!lookupRoute &&
|
|
3324
|
+
!isSmartRoutingGroup(group) &&
|
|
3325
|
+
!(await findRouteGroup(lookupRoute)) &&
|
|
3326
|
+
getVisibleServerByName(lookupRoute)?.tools.some((tool) => normalizeToolNameForServer(lookupRoute, tool.name) === request.params.name);
|
|
3285
3327
|
// Special handling for smart routing tools
|
|
3286
|
-
if (request.params.name === 'search_tools') {
|
|
3328
|
+
if (!upstreamOwnsMetaName && request.params.name === 'search_tools') {
|
|
3287
3329
|
const { query, limit = 10 } = request.params.arguments || {};
|
|
3288
3330
|
return await handleSearchToolsRequest(query, limit, sessionId, group);
|
|
3289
3331
|
}
|
|
3290
3332
|
// Special handling for describe_tool (progressive disclosure mode)
|
|
3291
|
-
if (request.params.name === 'describe_tool') {
|
|
3333
|
+
if (!upstreamOwnsMetaName && request.params.name === 'describe_tool') {
|
|
3292
3334
|
const { toolName } = request.params.arguments || {};
|
|
3293
3335
|
return await handleDescribeToolRequest(toolName, sessionId, group);
|
|
3294
3336
|
}
|
|
3295
3337
|
// Special handling for call_tool
|
|
3296
|
-
if (request.params.name === 'call_tool') {
|
|
3338
|
+
if (!upstreamOwnsMetaName && request.params.name === 'call_tool') {
|
|
3297
3339
|
const { toolName } = request.params.arguments || {};
|
|
3298
3340
|
if (!toolName) {
|
|
3299
3341
|
throw new Error('toolName parameter is required');
|
|
3300
3342
|
}
|
|
3301
3343
|
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;
|
|
3344
|
+
// Preserve legacy raw aggregate calls; explicit routes use the shared
|
|
3345
|
+
// resolver so cached names still obey group filtering.
|
|
3346
|
+
const singleServerAppsRoute = !getGroupLookupName(group) && !!appsRouteContext.serverInfo;
|
|
3307
3347
|
let targetServerInfo;
|
|
3308
3348
|
let targetToolName = toolName;
|
|
3309
3349
|
let targetTool;
|
|
@@ -3364,7 +3404,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3364
3404
|
arguments: summarizeArgumentsForLogging(finalArgs),
|
|
3365
3405
|
});
|
|
3366
3406
|
// Remove server prefix from tool name if present
|
|
3367
|
-
const cleanToolName = normalizeToolNameForServer(targetServerInfo.name,
|
|
3407
|
+
const cleanToolName = normalizeToolNameForServer(targetServerInfo.name, tool.name);
|
|
3368
3408
|
// Extract passthrough headers from extra or request context
|
|
3369
3409
|
let passthroughHeaders;
|
|
3370
3410
|
let requestHeaders = null;
|
|
@@ -3447,7 +3487,9 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3447
3487
|
if (!stateId) {
|
|
3448
3488
|
throw new Error(`Server '${targetServerInfo.name}' requires per-session client isolation and is not yet available on stateless MCP requests`);
|
|
3449
3489
|
}
|
|
3450
|
-
const isolated = await getOrCreateIsolatedClient(stateId, targetServerInfo)
|
|
3490
|
+
const isolated = await getOrCreateIsolatedClient(stateId, targetServerInfo, extra.mcpReq && !requestContextService.getRequestContext()?.stateless
|
|
3491
|
+
? servers[sessionId]
|
|
3492
|
+
: undefined);
|
|
3451
3493
|
isolatedCtx = {
|
|
3452
3494
|
sessionId: stateId,
|
|
3453
3495
|
client: isolated.client,
|
|
@@ -3465,12 +3507,12 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3465
3507
|
arguments: summarizeArgumentsForLogging(finalArgs),
|
|
3466
3508
|
perSessionClient: !!targetServerInfo.config?.perSessionClient,
|
|
3467
3509
|
});
|
|
3468
|
-
const cleanToolName = normalizeToolNameForServer(targetServerInfo.name,
|
|
3510
|
+
const cleanToolName = normalizeToolNameForServer(targetServerInfo.name, tool.name);
|
|
3469
3511
|
await reserveHostedIfNeeded(targetServerInfo.name, cleanToolName);
|
|
3470
3512
|
const result = await callToolWithReconnect(targetServerInfo, {
|
|
3471
3513
|
name: cleanToolName,
|
|
3472
3514
|
arguments: finalArgs,
|
|
3473
|
-
}, targetServerInfo.options || {}, 1, isolatedCtx);
|
|
3515
|
+
}, targetServerInfo.options || {}, 1, isolatedCtx, extra);
|
|
3474
3516
|
await settleHostedIfNeeded({
|
|
3475
3517
|
success: !result.isError,
|
|
3476
3518
|
requestContent: finalArgs,
|
|
@@ -3506,13 +3548,9 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3506
3548
|
}
|
|
3507
3549
|
// Regular tool handling
|
|
3508
3550
|
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;
|
|
3551
|
+
// Explicit routes share name resolution and group filtering regardless of
|
|
3552
|
+
// Apps support; keep legacy raw aggregate calls compatible.
|
|
3553
|
+
const singleServerAppsRoute = !getGroupLookupName(group) && !!appsRouteContext.serverInfo;
|
|
3516
3554
|
const groupTool = !singleServerAppsRoute && lookupGroup
|
|
3517
3555
|
? await resolveToolInGroup(group, request.params.name)
|
|
3518
3556
|
: undefined;
|
|
@@ -3540,7 +3578,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3540
3578
|
// For OpenAPI servers, use the OpenAPI client
|
|
3541
3579
|
const openApiClient = serverInfo.openApiClient;
|
|
3542
3580
|
// Remove server prefix from tool name if present
|
|
3543
|
-
const cleanToolName = normalizeToolNameForServer(serverInfo.name,
|
|
3581
|
+
const cleanToolName = normalizeToolNameForServer(serverInfo.name, tool.name);
|
|
3544
3582
|
logger.log('Invoking OpenAPI tool', {
|
|
3545
3583
|
toolName: cleanToolName,
|
|
3546
3584
|
serverName: serverInfo.name,
|
|
@@ -3628,15 +3666,17 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3628
3666
|
if (!stateId) {
|
|
3629
3667
|
throw new Error(`Server '${serverInfo.name}' requires per-session client isolation and is not yet available on stateless MCP requests`);
|
|
3630
3668
|
}
|
|
3631
|
-
const isolated = await getOrCreateIsolatedClient(stateId, serverInfo)
|
|
3669
|
+
const isolated = await getOrCreateIsolatedClient(stateId, serverInfo, extra.mcpReq && !requestContextService.getRequestContext()?.stateless
|
|
3670
|
+
? servers[sessionId]
|
|
3671
|
+
: undefined);
|
|
3632
3672
|
isolatedCtx = { sessionId: stateId, client: isolated.client, transport: isolated.transport };
|
|
3633
3673
|
}
|
|
3634
3674
|
else if (!serverInfo.client) {
|
|
3635
3675
|
throw new Error(`Client not found for server: ${serverInfo.name}`);
|
|
3636
3676
|
}
|
|
3637
|
-
const cleanToolName = normalizeToolNameForServer(serverInfo.name,
|
|
3677
|
+
const cleanToolName = normalizeToolNameForServer(serverInfo.name, tool.name);
|
|
3638
3678
|
await reserveHostedIfNeeded(serverInfo.name, cleanToolName);
|
|
3639
|
-
const result = await callToolWithReconnect(serverInfo, { ...request.params, name: cleanToolName }, serverInfo.options || {}, 1, isolatedCtx);
|
|
3679
|
+
const result = await callToolWithReconnect(serverInfo, { ...request.params, name: cleanToolName }, serverInfo.options || {}, 1, isolatedCtx, extra);
|
|
3640
3680
|
await settleHostedIfNeeded({
|
|
3641
3681
|
success: !result.isError,
|
|
3642
3682
|
requestContent: request.params.arguments,
|
|
@@ -3735,7 +3775,7 @@ const handleGetPromptRequestImpl = async (request, extra) => {
|
|
|
3735
3775
|
const { name, arguments: promptArgs } = request.params;
|
|
3736
3776
|
const group = getMcpRequestGroup(extra);
|
|
3737
3777
|
// Check built-in prompts first
|
|
3738
|
-
const builtinPrompt = await getBuiltinPromptDao().findByName(name);
|
|
3778
|
+
const builtinPrompt = !extra?.server && (await getBuiltinPromptDao().findByName(name));
|
|
3739
3779
|
if (builtinPrompt && builtinPrompt.enabled !== false) {
|
|
3740
3780
|
// Perform {{param}} template substitution
|
|
3741
3781
|
let content = builtinPrompt.template;
|
|
@@ -3757,7 +3797,12 @@ const handleGetPromptRequestImpl = async (request, extra) => {
|
|
|
3757
3797
|
let promptNameForServer = name;
|
|
3758
3798
|
const lookupGroup = getGroupLookupName(group);
|
|
3759
3799
|
if (extra && extra.server) {
|
|
3760
|
-
|
|
3800
|
+
const candidate = getVisibleServerByName(extra.server);
|
|
3801
|
+
const prompt = candidate && (await findEnabledPromptOnServer(candidate, name, true));
|
|
3802
|
+
if (prompt) {
|
|
3803
|
+
server = candidate;
|
|
3804
|
+
promptNameForServer = prompt.name;
|
|
3805
|
+
}
|
|
3761
3806
|
}
|
|
3762
3807
|
else if (lookupGroup) {
|
|
3763
3808
|
const groupPrompt = await resolvePromptInGroup(lookupGroup, name);
|
|
@@ -3767,10 +3812,14 @@ const handleGetPromptRequestImpl = async (request, extra) => {
|
|
|
3767
3812
|
}
|
|
3768
3813
|
}
|
|
3769
3814
|
else {
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3815
|
+
for (const candidate of getVisibleServerInfos()) {
|
|
3816
|
+
const prompt = await findEnabledPromptOnServer(candidate, name);
|
|
3817
|
+
if (prompt) {
|
|
3818
|
+
server = candidate;
|
|
3819
|
+
promptNameForServer = prompt.name;
|
|
3820
|
+
break;
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3774
3823
|
}
|
|
3775
3824
|
if (!server) {
|
|
3776
3825
|
throw new ToolUnavailableError(`Prompt not available: ${name}`, classifyUnavailableReason(name));
|
|
@@ -3839,7 +3888,7 @@ const handleListPromptsRequestImpl = async (_, extra) => {
|
|
|
3839
3888
|
let enabledPrompts = serverInfo.prompts;
|
|
3840
3889
|
if (serverConfig && serverConfig.prompts) {
|
|
3841
3890
|
enabledPrompts = serverInfo.prompts.filter((prompt) => {
|
|
3842
|
-
const promptConfig = serverConfig.prompts
|
|
3891
|
+
const promptConfig = findToolConfig(serverConfig.prompts, serverInfo.name, prompt.name, serverInfo.prompts.map((item) => item.name));
|
|
3843
3892
|
// If prompt is not in config, it's enabled by default
|
|
3844
3893
|
return promptConfig?.enabled !== false;
|
|
3845
3894
|
});
|
|
@@ -4006,6 +4055,12 @@ const handleReadResourceRequestImpl = async (request, extra) => {
|
|
|
4006
4055
|
? [appsRouteContext.serverInfo]
|
|
4007
4056
|
: (appsRouteContext.serverInfos ?? []);
|
|
4008
4057
|
for (const candidate of candidates) {
|
|
4058
|
+
const config = await getServerDao().findById(candidate.name);
|
|
4059
|
+
if (config?.resources?.[uri]?.enabled === false)
|
|
4060
|
+
continue;
|
|
4061
|
+
const allowed = await filterResourcesByGroup(lookupGroup, candidate.name, [{ uri }], serverConfigsByName.get(candidate.name));
|
|
4062
|
+
if (allowed.length === 0)
|
|
4063
|
+
continue;
|
|
4009
4064
|
// An on-demand candidate may be idle yet still advertise its cached widget
|
|
4010
4065
|
// link, so a host can read the ui:// resource before any tool call has
|
|
4011
4066
|
// woken it. Hold off its idle shutdown for every read, awake or not, so an
|
|
@@ -4201,7 +4256,7 @@ const withPrincipalServers = (handler, operation) => async (request, extra) => {
|
|
|
4201
4256
|
if (!serverInfos.some((info) => hasPersonalAuthentication(info.config)))
|
|
4202
4257
|
return handler(request, extra);
|
|
4203
4258
|
// Built-in content does not require an upstream credential.
|
|
4204
|
-
if (operation === 'prompt') {
|
|
4259
|
+
if (operation === 'prompt' && !extra?.server) {
|
|
4205
4260
|
const builtin = await getBuiltinPromptDao().findByName(request.params?.name);
|
|
4206
4261
|
if (builtin && builtin.enabled !== false)
|
|
4207
4262
|
return handler(request, extra);
|