@samanhappy/mcphub 1.1.9 → 1.1.11
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/legacyMcpClient.js +10 -2
- package/dist/clients/legacyMcpClient.js.map +1 -1
- package/dist/controllers/groupController.js +19 -0
- package/dist/controllers/groupController.js.map +1 -1
- package/dist/controllers/serverController.js +2 -0
- package/dist/controllers/serverController.js.map +1 -1
- package/dist/dao/CapabilitySnapshotDao.js +38 -0
- package/dist/dao/CapabilitySnapshotDao.js.map +1 -0
- package/dist/dao/CapabilitySnapshotDaoDbImpl.js +18 -0
- package/dist/dao/CapabilitySnapshotDaoDbImpl.js.map +1 -0
- package/dist/dao/DaoFactory.js +7 -0
- package/dist/dao/DaoFactory.js.map +1 -1
- package/dist/dao/DatabaseDaoFactory.js +4 -0
- package/dist/dao/DatabaseDaoFactory.js.map +1 -1
- package/dist/dao/ServerDaoDbImpl.js +3 -0
- package/dist/dao/ServerDaoDbImpl.js.map +1 -1
- package/dist/db/entities/CapabilitySnapshot.js +37 -0
- package/dist/db/entities/CapabilitySnapshot.js.map +1 -0
- package/dist/db/entities/Group.js.map +1 -1
- package/dist/db/entities/Server.js +4 -0
- package/dist/db/entities/Server.js.map +1 -1
- package/dist/db/entities/index.js +3 -1
- package/dist/db/entities/index.js.map +1 -1
- package/dist/services/capabilitySnapshotService.js +106 -0
- package/dist/services/capabilitySnapshotService.js.map +1 -0
- package/dist/services/groupService.js +1 -0
- package/dist/services/groupService.js.map +1 -1
- package/dist/services/keepAliveService.js +52 -36
- package/dist/services/keepAliveService.js.map +1 -1
- package/dist/services/mcpOAuthProvider.js +19 -2
- package/dist/services/mcpOAuthProvider.js.map +1 -1
- package/dist/services/mcpService.js +408 -103
- package/dist/services/mcpService.js.map +1 -1
- package/dist/services/oauthClientRegistration.js +2 -1
- package/dist/services/oauthClientRegistration.js.map +1 -1
- package/dist/services/principalRuntimeService.js +27 -3
- package/dist/services/principalRuntimeService.js.map +1 -1
- package/dist/services/smartRoutingService.js +19 -5
- package/dist/services/smartRoutingService.js.map +1 -1
- package/dist/services/templateService.js +5 -0
- package/dist/services/templateService.js.map +1 -1
- package/dist/services/upstreamListRefresh.js +37 -0
- package/dist/services/upstreamListRefresh.js.map +1 -0
- package/dist/utils/groupToolNames.js +21 -0
- package/dist/utils/groupToolNames.js.map +1 -0
- package/dist/utils/mcpProtocol.js +14 -1
- package/dist/utils/mcpProtocol.js.map +1 -1
- package/dist/utils/migration.js +1 -0
- package/dist/utils/migration.js.map +1 -1
- package/dist/utils/oauthServerMetadata.js +23 -0
- package/dist/utils/oauthServerMetadata.js.map +1 -0
- package/dist/utils/parameterConversion.js +9 -2
- package/dist/utils/parameterConversion.js.map +1 -1
- package/dist/utils/serverConfigPersistence.js +11 -0
- package/dist/utils/serverConfigPersistence.js.map +1 -1
- package/dist/utils/settingsSync.js +2 -0
- package/dist/utils/settingsSync.js.map +1 -1
- package/frontend/dist/assets/{ActivityPage-CFRX20v4.js → ActivityPage-D8nsR_f_.js} +2 -2
- package/frontend/dist/assets/{ActivityPage-CFRX20v4.js.map → ActivityPage-D8nsR_f_.js.map} +1 -1
- package/frontend/dist/assets/{CopyClientConfigDialog-CDEiSInj.js → CopyClientConfigDialog-DgHrZUv_.js} +2 -2
- package/frontend/dist/assets/{CopyClientConfigDialog-CDEiSInj.js.map → CopyClientConfigDialog-DgHrZUv_.js.map} +1 -1
- package/frontend/dist/assets/{CredentialsPage-BCLHY_fN.js → CredentialsPage-B_-H2SKV.js} +2 -2
- package/frontend/dist/assets/{CredentialsPage-BCLHY_fN.js.map → CredentialsPage-B_-H2SKV.js.map} +1 -1
- package/frontend/dist/assets/{Dashboard-B3Y6nAFL.js → Dashboard-Bhl6u2n-.js} +2 -2
- package/frontend/dist/assets/{Dashboard-B3Y6nAFL.js.map → Dashboard-Bhl6u2n-.js.map} +1 -1
- package/frontend/dist/assets/{EndpointCopy-BMxT99Ne.js → EndpointCopy-Cbm48MAT.js} +2 -2
- package/frontend/dist/assets/{EndpointCopy-BMxT99Ne.js.map → EndpointCopy-Cbm48MAT.js.map} +1 -1
- package/frontend/dist/assets/GroupsPage-DhBTIL-O.js +33 -0
- package/frontend/dist/assets/GroupsPage-DhBTIL-O.js.map +1 -0
- package/frontend/dist/assets/{LoginPage-BjYzY6RH.js → LoginPage-DuyaaEX3.js} +2 -2
- package/frontend/dist/assets/{LoginPage-BjYzY6RH.js.map → LoginPage-DuyaaEX3.js.map} +1 -1
- package/frontend/dist/assets/{LogsPage-CGcFNKJn.js → LogsPage-BtYlQ3Fg.js} +2 -2
- package/frontend/dist/assets/{LogsPage-CGcFNKJn.js.map → LogsPage-BtYlQ3Fg.js.map} +1 -1
- package/frontend/dist/assets/{MarketPage-B7JPqFw3.js → MarketPage-QNT3w149.js} +2 -2
- package/frontend/dist/assets/{MarketPage-B7JPqFw3.js.map → MarketPage-QNT3w149.js.map} +1 -1
- package/frontend/dist/assets/{OAuthConsentPage-BYi_kgNj.js → OAuthConsentPage-fjVkCE13.js} +2 -2
- package/frontend/dist/assets/{OAuthConsentPage-BYi_kgNj.js.map → OAuthConsentPage-fjVkCE13.js.map} +1 -1
- package/frontend/dist/assets/{PromptsPage-x6X5trLX.js → PromptsPage-CWQEgxAH.js} +2 -2
- package/frontend/dist/assets/{PromptsPage-x6X5trLX.js.map → PromptsPage-CWQEgxAH.js.map} +1 -1
- package/frontend/dist/assets/{ResourcesPage-BJZCPBs8.js → ResourcesPage-Dw3p1r3S.js} +2 -2
- package/frontend/dist/assets/{ResourcesPage-BJZCPBs8.js.map → ResourcesPage-Dw3p1r3S.js.map} +1 -1
- package/frontend/dist/assets/ServersPage-C9F8T1eo.js +39 -0
- package/frontend/dist/assets/ServersPage-C9F8T1eo.js.map +1 -0
- package/frontend/dist/assets/{SettingsPage-C53DI0Sz.js → SettingsPage-DGLThCbs.js} +2 -2
- package/frontend/dist/assets/{SettingsPage-C53DI0Sz.js.map → SettingsPage-DGLThCbs.js.map} +1 -1
- package/frontend/dist/assets/{StatusDot-BDJd3Iix.js → StatusDot-L_s7TahY.js} +2 -2
- package/frontend/dist/assets/{StatusDot-BDJd3Iix.js.map → StatusDot-L_s7TahY.js.map} +1 -1
- package/frontend/dist/assets/{ToggleGroup-BbmRhILg.js → ToggleGroup-BYsHpk5D.js} +2 -2
- package/frontend/dist/assets/{ToggleGroup-BbmRhILg.js.map → ToggleGroup-BYsHpk5D.js.map} +1 -1
- package/frontend/dist/assets/{UsersPage-NUG6vF8C.js → UsersPage-uH8VPdD5.js} +2 -2
- package/frontend/dist/assets/{UsersPage-NUG6vF8C.js.map → UsersPage-uH8VPdD5.js.map} +1 -1
- package/frontend/dist/assets/{clipboard-7BggsilD.js → clipboard-7o0A3Yh4.js} +2 -2
- package/frontend/dist/assets/{clipboard-7BggsilD.js.map → clipboard-7o0A3Yh4.js.map} +1 -1
- package/frontend/dist/assets/index-Bflb_BXR.js +3 -0
- package/frontend/dist/assets/index-Bflb_BXR.js.map +1 -0
- package/frontend/dist/assets/index-JDk4RjZI.css +1 -0
- package/frontend/dist/assets/{resourceService-B674Fopr.js → resourceService-0jFnNuzj.js} +2 -2
- package/frontend/dist/assets/{resourceService-B674Fopr.js.map → resourceService-0jFnNuzj.js.map} +1 -1
- package/frontend/dist/assets/{useCostData-8Z1HjAL5.js → useCostData-BrYWvkNn.js} +2 -2
- package/frontend/dist/assets/{useCostData-8Z1HjAL5.js.map → useCostData-BrYWvkNn.js.map} +1 -1
- package/frontend/dist/assets/variableDetection-D0qCtrcY.js +18 -0
- package/frontend/dist/assets/variableDetection-D0qCtrcY.js.map +1 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/GroupsPage-5iCk1LKT.js +0 -33
- package/frontend/dist/assets/GroupsPage-5iCk1LKT.js.map +0 -1
- package/frontend/dist/assets/ServersPage-CsyGLden.js +0 -39
- package/frontend/dist/assets/ServersPage-CsyGLden.js.map +0 -1
- package/frontend/dist/assets/index-B_IITM9e.js +0 -3
- package/frontend/dist/assets/index-B_IITM9e.js.map +0 -1
- package/frontend/dist/assets/index-CnqLKewL.css +0 -1
- package/frontend/dist/assets/variableDetection-C0nP3Jj2.js +0 -18
- package/frontend/dist/assets/variableDetection-C0nP3Jj2.js.map +0 -1
|
@@ -1,18 +1,21 @@
|
|
|
1
|
+
import { projectGroupToolName, assertValidGroupToolNames } from '../utils/groupToolNames.js';
|
|
1
2
|
import { isSafePassthroughHeader } from '../utils/passthroughHeaders.js';
|
|
2
3
|
import { observeToolCall, setMetricsServer } from './metricsService.js';
|
|
3
4
|
import { hasPersonalOAuth, PersonalOAuthContext, personalOAuthContexts, personalAuthorizationRequired, } from './personalOAuthService.js';
|
|
4
5
|
import { preserveListFreshness, combineListFreshness, remainingListTtl, } from '../utils/listFreshness.js';
|
|
5
6
|
import { expandServerConfig, getServerEnvironment } from './serverConfigEnvironment.js';
|
|
6
7
|
import { getMcpRequestGroup } from '../utils/mcpRequestGroup.js';
|
|
8
|
+
import { revalidateUpstreamList } from './upstreamListRefresh.js';
|
|
7
9
|
import { LegacyMcpClient } from '../clients/legacyMcpClient.js';
|
|
8
10
|
import { getElicitationCapabilities, installElicitationRelay, isElicitationClientClosed, withElicitation, } from './elicitationService.js';
|
|
9
|
-
import { LEGACY_PROTOCOL_VERSIONS } from '../utils/mcpProtocol.js';
|
|
11
|
+
import { LEGACY_PROTOCOL_VERSIONS, resolveUpstreamProtocolMode } from '../utils/mcpProtocol.js';
|
|
10
12
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
11
13
|
import { createHash } from 'node:crypto';
|
|
12
14
|
import { canAccessGroupRoute } from '../utils/groupAccess.js';
|
|
13
15
|
import { canServeToolRequests } from '../utils/serverAvailability.js';
|
|
14
16
|
import { assertToolCallArgumentsSafe, isToolCallGuardEnabled, ToolCallBlockedError, } from '../middlewares/toolCallGuard.js';
|
|
15
17
|
import { applyHiddenParams, findToolConfig, removeHiddenArguments, } from '../utils/toolConfig.js';
|
|
18
|
+
import { prepareCapabilitySnapshot, recordCapabilitySnapshot, flushCapabilitySnapshot, } from './capabilitySnapshotService.js';
|
|
16
19
|
import { PrincipalRuntimeService } from './principalRuntimeService.js';
|
|
17
20
|
import { UserContextService } from './userContextService.js';
|
|
18
21
|
import { deleteCredentialBindings, missingCredentialError } from './credentialBindingService.js';
|
|
@@ -389,7 +392,7 @@ const getOrCreateIsolatedClient = async (sessionId, serverInfo, downstream) => {
|
|
|
389
392
|
}
|
|
390
393
|
const reservedClients = sessionIsolatedClients.get(sessionId);
|
|
391
394
|
const transport = await createTransportFromConfig(serverInfo.name, serverConfig);
|
|
392
|
-
const client = createUpstreamMcpClient(serverInfo.name, () => serverInfo, downstream);
|
|
395
|
+
const client = createUpstreamMcpClient(serverInfo.name, () => serverInfo, serverConfig, downstream);
|
|
393
396
|
try {
|
|
394
397
|
await connectClientWithDiagnostics(client, transport, serverInfo.options || {});
|
|
395
398
|
}
|
|
@@ -723,6 +726,8 @@ const createConnectGate = (limit) => {
|
|
|
723
726
|
// result anywhere. See the `liveServerInfo` helper in that function.
|
|
724
727
|
let initGeneration = 0;
|
|
725
728
|
const principalServerScope = new AsyncLocalStorage();
|
|
729
|
+
// Header-dependent discovery must never publish descriptors or embeddings globally.
|
|
730
|
+
const requestDiscoveryInfos = new WeakSet();
|
|
726
731
|
const getVisibleServerInfos = () => {
|
|
727
732
|
return getDataService().filterData(principalServerScope.getStore() ?? serverInfos);
|
|
728
733
|
};
|
|
@@ -825,6 +830,8 @@ export const setServerInfosForTest = (infos) => {
|
|
|
825
830
|
serverInfos = infos;
|
|
826
831
|
};
|
|
827
832
|
export const updateServerToolsCache = (serverInfo, tools, options) => {
|
|
833
|
+
if (!options?.restored)
|
|
834
|
+
recordCapabilitySnapshot(serverInfo, tools);
|
|
828
835
|
serverInfo.tools = preserveListFreshness(tools, tools.map((tool) => normalizeToolForCache(serverInfo.name, tool)));
|
|
829
836
|
syncToolsAsVectorEmbeddings(serverInfo.name, serverInfo.tools, {
|
|
830
837
|
reportProgress: options?.reportEmbeddingProgress === true,
|
|
@@ -838,15 +845,81 @@ const updateServerPromptsCache = (serverInfo, prompts) => {
|
|
|
838
845
|
const updateServerResourcesCache = (serverInfo, resources) => {
|
|
839
846
|
serverInfo.resources = preserveListFreshness(resources, resources.map(normalizeResourceForCache));
|
|
840
847
|
};
|
|
848
|
+
// Modern HTTP has no list-changed push stream. Refresh only the lists needed
|
|
849
|
+
// by the current request; TTL and in-flight work are scoped to the live client.
|
|
850
|
+
const refreshModernServerLists = async (info, fields) => {
|
|
851
|
+
const client = info.client;
|
|
852
|
+
if (!client ||
|
|
853
|
+
client.getProtocolEra?.() !== 'modern' ||
|
|
854
|
+
info.enabled === false ||
|
|
855
|
+
info.status !== 'connected')
|
|
856
|
+
return;
|
|
857
|
+
const capabilities = client.getServerCapabilities();
|
|
858
|
+
const current = () => info.client === client && info.enabled !== false && info.status === 'connected';
|
|
859
|
+
await Promise.all(fields.map(async (field) => {
|
|
860
|
+
if (!capabilities?.[field])
|
|
861
|
+
return;
|
|
862
|
+
await revalidateUpstreamList(client, field, async () => {
|
|
863
|
+
if (field === 'tools') {
|
|
864
|
+
const { tools } = await client.listTools({}, info.options || {});
|
|
865
|
+
if (!current())
|
|
866
|
+
return;
|
|
867
|
+
if (hasPersonalAuthentication(info.config) || requestDiscoveryInfos.has(info)) {
|
|
868
|
+
info.tools = preserveListFreshness(tools, tools.map((tool) => normalizeToolForCache(info.name, tool)));
|
|
869
|
+
if (!requestDiscoveryInfos.has(info))
|
|
870
|
+
recordCapabilitySnapshot(info, tools);
|
|
871
|
+
if (!requestDiscoveryInfos.has(info))
|
|
872
|
+
void syncCredentialServerToolEmbeddings(info.name, info.tools, {
|
|
873
|
+
partial: true,
|
|
874
|
+
}).catch(() => {
|
|
875
|
+
logger.warn('[EMBED_SYNC_ERROR] Failed to sync personal tool embeddings');
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
else {
|
|
879
|
+
const previous = JSON.stringify(info.tools);
|
|
880
|
+
updateServerToolsCache(info, tools);
|
|
881
|
+
if (previous !== JSON.stringify(info.tools))
|
|
882
|
+
broadcastToolListChanged();
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
else if (field === 'prompts') {
|
|
886
|
+
const { prompts } = await client.listPrompts({}, info.options || {});
|
|
887
|
+
if (current()) {
|
|
888
|
+
const previous = JSON.stringify(info.prompts);
|
|
889
|
+
updateServerPromptsCache(info, prompts);
|
|
890
|
+
if (!hasPersonalAuthentication(info.config) &&
|
|
891
|
+
!requestDiscoveryInfos.has(info) &&
|
|
892
|
+
previous !== JSON.stringify(info.prompts))
|
|
893
|
+
broadcastPromptListChanged();
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
else {
|
|
897
|
+
const { resources } = await client.listResources({}, info.options || {});
|
|
898
|
+
if (current()) {
|
|
899
|
+
const previous = JSON.stringify(info.resources);
|
|
900
|
+
updateServerResourcesCache(info, resources);
|
|
901
|
+
if (!hasPersonalAuthentication(info.config) &&
|
|
902
|
+
!requestDiscoveryInfos.has(info) &&
|
|
903
|
+
previous !== JSON.stringify(info.resources))
|
|
904
|
+
broadcastResourceListChanged();
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
}));
|
|
909
|
+
};
|
|
841
910
|
const logListChangedRefreshError = (listType) => {
|
|
842
911
|
logger.warn(`Failed to refresh ${listType} list after upstream notification`);
|
|
843
912
|
};
|
|
844
|
-
const createUpstreamMcpClient = (name, getServerInfo, downstream) => {
|
|
913
|
+
const createUpstreamMcpClient = (name, getServerInfo, serverConfig, downstream) => {
|
|
845
914
|
const elicitation = getElicitationCapabilities(downstream);
|
|
915
|
+
// The existing relay only supports legacy server-to-client requests. Keep
|
|
916
|
+
// elicitation-enabled isolated clients legacy until an input_required bridge exists.
|
|
917
|
+
const mode = resolveUpstreamProtocolMode(serverConfig, Boolean(elicitation));
|
|
846
918
|
const client = new LegacyMcpClient({
|
|
847
919
|
name: `mcp-client-${name}`,
|
|
848
920
|
version: '1.0.0',
|
|
849
921
|
}, {
|
|
922
|
+
versionNegotiation: { mode },
|
|
850
923
|
capabilities: { ...MCP_APPS_CAPABILITIES, ...(elicitation ? { elicitation } : {}) },
|
|
851
924
|
jsonSchemaValidator: new ResilientJsonSchemaValidator(),
|
|
852
925
|
listChanged: {
|
|
@@ -958,19 +1031,8 @@ export const cleanupAllServers = () => {
|
|
|
958
1031
|
for (const id of clientStates.keys())
|
|
959
1032
|
removeClientState(id);
|
|
960
1033
|
principalRuntimes.invalidate();
|
|
961
|
-
for (const serverInfo of serverInfos)
|
|
962
|
-
|
|
963
|
-
if (serverInfo.client) {
|
|
964
|
-
serverInfo.client.close();
|
|
965
|
-
}
|
|
966
|
-
if (serverInfo.transport) {
|
|
967
|
-
serverInfo.transport.close();
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
catch (error) {
|
|
971
|
-
logger.warn('Error closing server', { serverName: serverInfo.name, error });
|
|
972
|
-
}
|
|
973
|
-
}
|
|
1034
|
+
for (const serverInfo of serverInfos)
|
|
1035
|
+
closeServerRuntime(serverInfo);
|
|
974
1036
|
serverInfos = [];
|
|
975
1037
|
// Drain all per-session isolated upstream clients (perSessionClient: true),
|
|
976
1038
|
for (const sessionId of [...sessionIsolatedClients.keys()]) {
|
|
@@ -1374,6 +1436,38 @@ const withoutHiddenArguments = (serverInfo, toolName, args) => {
|
|
|
1374
1436
|
const toolConfig = findToolConfig(serverInfo.config?.tools, serverInfo.name, runtimeName, (serverInfo.tools ?? []).map((candidate) => candidate.name));
|
|
1375
1437
|
return removeHiddenArguments(args, tool?.inputSchema, toolConfig);
|
|
1376
1438
|
};
|
|
1439
|
+
const failedCallRefreshes = new WeakMap();
|
|
1440
|
+
const refreshCapabilitiesAfterFailedCall = async (info) => {
|
|
1441
|
+
if (!info.capabilityCache || !info.client || info.status !== 'connected')
|
|
1442
|
+
return;
|
|
1443
|
+
const pending = failedCallRefreshes.get(info);
|
|
1444
|
+
if (pending)
|
|
1445
|
+
return pending;
|
|
1446
|
+
const refresh = (async () => {
|
|
1447
|
+
try {
|
|
1448
|
+
const { tools } = await info.client.listTools({}, info.options || {});
|
|
1449
|
+
if (hasPersonalAuthentication(info.config)) {
|
|
1450
|
+
info.tools = tools.map((tool) => normalizeToolForCache(info.name, tool));
|
|
1451
|
+
recordCapabilitySnapshot(info, tools);
|
|
1452
|
+
}
|
|
1453
|
+
else {
|
|
1454
|
+
updateServerToolsCache(info, tools);
|
|
1455
|
+
broadcastToolListChanged();
|
|
1456
|
+
}
|
|
1457
|
+
await flushCapabilitySnapshot(info);
|
|
1458
|
+
}
|
|
1459
|
+
catch {
|
|
1460
|
+
logger.warn('Unable to refresh capability snapshot after failed call');
|
|
1461
|
+
}
|
|
1462
|
+
})();
|
|
1463
|
+
failedCallRefreshes.set(info, refresh);
|
|
1464
|
+
try {
|
|
1465
|
+
await refresh;
|
|
1466
|
+
}
|
|
1467
|
+
finally {
|
|
1468
|
+
failedCallRefreshes.delete(info);
|
|
1469
|
+
}
|
|
1470
|
+
};
|
|
1377
1471
|
// Helper function to handle client.callTool with reconnection logic
|
|
1378
1472
|
const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries = 1, isolated, extra) => {
|
|
1379
1473
|
// Local, reassignable refs so a reconnect can swap in the new connection for
|
|
@@ -1390,19 +1484,37 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
|
|
|
1390
1484
|
arguments: withoutHiddenArguments(serverInfo, toolParams.name, toolParams.arguments),
|
|
1391
1485
|
};
|
|
1392
1486
|
if (hasPersonalAuthentication(serverInfo.config)) {
|
|
1393
|
-
|
|
1487
|
+
try {
|
|
1488
|
+
const result = await client.callTool(toolParams, options || {});
|
|
1489
|
+
if (result.isError)
|
|
1490
|
+
await refreshCapabilitiesAfterFailedCall(serverInfo);
|
|
1491
|
+
return result;
|
|
1492
|
+
}
|
|
1493
|
+
catch (error) {
|
|
1494
|
+
await refreshCapabilitiesAfterFailedCall(serverInfo);
|
|
1495
|
+
throw error;
|
|
1496
|
+
}
|
|
1394
1497
|
}
|
|
1395
1498
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1396
1499
|
try {
|
|
1397
1500
|
const currentClient = client;
|
|
1398
1501
|
const result = await withElicitation(currentClient, extra?.mcpReq, options || {}, async (callOptions) => {
|
|
1399
|
-
|
|
1502
|
+
// Isolated clients do not own the refreshed discovery client's tool definitions.
|
|
1503
|
+
const toolDefinition = isolated && currentClient.getProtocolEra?.() === 'modern'
|
|
1504
|
+
? serverInfo.tools.find((tool) => normalizeToolNameForServer(serverInfo.name, tool.name) === toolParams.name)
|
|
1505
|
+
: undefined;
|
|
1506
|
+
const result = await currentClient.callTool(toolParams, toolDefinition
|
|
1507
|
+
? { ...callOptions, toolDefinition: toolDefinition }
|
|
1508
|
+
: callOptions);
|
|
1400
1509
|
checkAuthError(result);
|
|
1401
1510
|
return result;
|
|
1402
1511
|
});
|
|
1512
|
+
if (result.isError)
|
|
1513
|
+
await refreshCapabilitiesAfterFailedCall(serverInfo);
|
|
1403
1514
|
return result;
|
|
1404
1515
|
}
|
|
1405
1516
|
catch (error) {
|
|
1517
|
+
await refreshCapabilitiesAfterFailedCall(serverInfo);
|
|
1406
1518
|
if (isElicitationClientClosed(client)) {
|
|
1407
1519
|
closeIsolatedClient(serverInfo.name, client, transport);
|
|
1408
1520
|
throw error;
|
|
@@ -1427,7 +1539,7 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
|
|
|
1427
1539
|
// Match initial connection expansion before URL validation and transport creation.
|
|
1428
1540
|
const server = await expandServerConfig(rawConfig);
|
|
1429
1541
|
const newTransport = await createTransportFromConfig(serverInfo.name, server);
|
|
1430
|
-
const newClient = createUpstreamMcpClient(serverInfo.name, () => serverInfo);
|
|
1542
|
+
const newClient = createUpstreamMcpClient(serverInfo.name, () => serverInfo, server);
|
|
1431
1543
|
// Reconnect with new transport
|
|
1432
1544
|
await connectClientWithDiagnostics(newClient, newTransport, serverInfo.options || {});
|
|
1433
1545
|
if (isolated) {
|
|
@@ -1455,6 +1567,8 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
|
|
|
1455
1567
|
serverInfo.keepAliveIntervalId = undefined;
|
|
1456
1568
|
}
|
|
1457
1569
|
try {
|
|
1570
|
+
if (serverInfo.client)
|
|
1571
|
+
serverInfo.client.onclose = undefined;
|
|
1458
1572
|
serverInfo.client?.close();
|
|
1459
1573
|
}
|
|
1460
1574
|
catch {
|
|
@@ -1498,6 +1612,8 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
|
|
|
1498
1612
|
if (!isolated) {
|
|
1499
1613
|
serverInfo.status = 'disconnected';
|
|
1500
1614
|
serverInfo.error = `Failed to reconnect: ${formatErrorForLogging(reconnectError)}`;
|
|
1615
|
+
if (serverInfo.config)
|
|
1616
|
+
setupServerKeepAlive(serverInfo, serverInfo.config);
|
|
1501
1617
|
}
|
|
1502
1618
|
// If this was the last attempt, throw the original error
|
|
1503
1619
|
if (attempt === maxRetries) {
|
|
@@ -1506,7 +1622,17 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
|
|
|
1506
1622
|
}
|
|
1507
1623
|
}
|
|
1508
1624
|
else {
|
|
1509
|
-
|
|
1625
|
+
const status = error?.status ?? error?.response?.status ?? error?.code;
|
|
1626
|
+
const connectionFailed = isUpstreamConnectionFailure(error) ||
|
|
1627
|
+
isUpstreamConnectionFailure(error?.cause) ||
|
|
1628
|
+
(error instanceof TypeError && error.message === 'fetch failed') ||
|
|
1629
|
+
(typeof status === 'number' && status >= 500 && status < 600);
|
|
1630
|
+
if (!isolated && (isStreamableHttp || isSSE) && connectionFailed) {
|
|
1631
|
+
serverInfo.status = 'disconnected';
|
|
1632
|
+
serverInfo.error = formatErrorForLogging(error);
|
|
1633
|
+
if (serverInfo.config)
|
|
1634
|
+
setupServerKeepAlive(serverInfo, serverInfo.config);
|
|
1635
|
+
}
|
|
1510
1636
|
throw error;
|
|
1511
1637
|
}
|
|
1512
1638
|
}
|
|
@@ -1515,16 +1641,22 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
|
|
|
1515
1641
|
throw new Error('Unexpected error in callToolWithReconnect');
|
|
1516
1642
|
};
|
|
1517
1643
|
const setupServerKeepAlive = (serverInfo, serverConfig) => {
|
|
1644
|
+
const client = serverInfo.client;
|
|
1645
|
+
if (client &&
|
|
1646
|
+
serverInfo.status === 'connected' &&
|
|
1647
|
+
(serverInfo.transport instanceof SSEClientTransport ||
|
|
1648
|
+
serverInfo.transport instanceof StreamableHTTPClientTransport)) {
|
|
1649
|
+
client.onclose = () => {
|
|
1650
|
+
const current = getServerByName(serverInfo.name);
|
|
1651
|
+
if (current?.client !== client || current.status !== 'connected')
|
|
1652
|
+
return;
|
|
1653
|
+
current.status = 'disconnected';
|
|
1654
|
+
current.error = 'Upstream connection closed';
|
|
1655
|
+
setupServerKeepAlive(current, serverConfig);
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1518
1658
|
setupClientKeepAlive(serverInfo, serverConfig, {
|
|
1519
|
-
reconnectServer
|
|
1520
|
-
try {
|
|
1521
|
-
await reconnectServer(serverName);
|
|
1522
|
-
}
|
|
1523
|
-
catch (error) {
|
|
1524
|
-
setupServerKeepAlive(serverInfo, serverConfig);
|
|
1525
|
-
throw error;
|
|
1526
|
-
}
|
|
1527
|
-
},
|
|
1659
|
+
reconnectServer,
|
|
1528
1660
|
}).catch((error) => logger.warn('Keepalive setup failed', {
|
|
1529
1661
|
serverName: serverInfo.name,
|
|
1530
1662
|
error: summarizeErrorForLogging(error),
|
|
@@ -1602,6 +1734,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1602
1734
|
// connection) so the tool list stays visible to agents even while sleeping.
|
|
1603
1735
|
if (expandedConf.startOnDemand === true && isStdioServer(expandedConf)) {
|
|
1604
1736
|
const existingOnDemand = existingServerInfos.find((s) => s.name === name);
|
|
1737
|
+
const configChanged = JSON.stringify(existingOnDemand?.config) !== JSON.stringify(expandedConf);
|
|
1605
1738
|
// Active calls, spawn promises and idle callbacks must share the same
|
|
1606
1739
|
// runtime object across reloads, not copies of its mutable state.
|
|
1607
1740
|
const onDemandInfo = existingOnDemand ?? {
|
|
@@ -1620,6 +1753,14 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1620
1753
|
enabled: true,
|
|
1621
1754
|
config: expandedConf,
|
|
1622
1755
|
});
|
|
1756
|
+
const cachedTools = await prepareCapabilitySnapshot(onDemandInfo, expandedConf);
|
|
1757
|
+
if (cachedTools !== undefined) {
|
|
1758
|
+
if (!existingOnDemand || configChanged || onDemandInfo.tools.length === 0)
|
|
1759
|
+
updateServerToolsCache(onDemandInfo, cachedTools, { restored: true });
|
|
1760
|
+
}
|
|
1761
|
+
else if (configChanged || onDemandInfo.capabilityCache?.state === 'stale') {
|
|
1762
|
+
onDemandInfo.tools = [];
|
|
1763
|
+
}
|
|
1623
1764
|
nextServerInfos.push(onDemandInfo);
|
|
1624
1765
|
logger.log(`Skipping startup connect for on-demand server: ${name}`);
|
|
1625
1766
|
continue;
|
|
@@ -1641,18 +1782,25 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1641
1782
|
if (existingServer &&
|
|
1642
1783
|
(isDifferentServer ||
|
|
1643
1784
|
(!serverName && (existingServer.status === 'connected' || hasInflightOAuthAuthorization)))) {
|
|
1644
|
-
|
|
1645
|
-
|
|
1785
|
+
// Timer callbacks must keep owning the published runtime entry.
|
|
1786
|
+
Object.assign(existingServer, {
|
|
1646
1787
|
owner: expandedConf.owner,
|
|
1647
1788
|
visibility: expandedConf.visibility,
|
|
1648
1789
|
sharedWithUsers: expandedConf.sharedWithUsers,
|
|
1649
1790
|
enabled: expandedConf.enabled === undefined ? true : expandedConf.enabled,
|
|
1650
1791
|
});
|
|
1792
|
+
nextServerInfos.push(existingServer);
|
|
1651
1793
|
logger.log(hasInflightOAuthAuthorization
|
|
1652
1794
|
? `Server '${name}' has an in-flight OAuth authorization; preserving existing state.`
|
|
1653
1795
|
: `Server '${name}' is already connected.`);
|
|
1654
1796
|
continue;
|
|
1655
1797
|
}
|
|
1798
|
+
if (existingServer?.keepAliveIntervalId) {
|
|
1799
|
+
clearInterval(existingServer.keepAliveIntervalId);
|
|
1800
|
+
existingServer.keepAliveIntervalId = undefined;
|
|
1801
|
+
}
|
|
1802
|
+
if (existingServer?.client)
|
|
1803
|
+
existingServer.client.onclose = undefined;
|
|
1656
1804
|
let transport;
|
|
1657
1805
|
let openApiClient;
|
|
1658
1806
|
if (expandedConf.type === 'openapi') {
|
|
@@ -1785,7 +1933,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1785
1933
|
}
|
|
1786
1934
|
}
|
|
1787
1935
|
const serverInfoRef = {};
|
|
1788
|
-
const client = createUpstreamMcpClient(name, () => serverInfoRef.current);
|
|
1936
|
+
const client = createUpstreamMcpClient(name, () => serverInfoRef.current, expandedConf);
|
|
1789
1937
|
// Get request options from server configuration, with fallbacks
|
|
1790
1938
|
const serverRequestOptions = expandedConf.options || {};
|
|
1791
1939
|
const defaultRequestTimeout = Number(process.env.DEFAULT_REQUEST_TIMEOUT) || 60000;
|
|
@@ -1807,7 +1955,8 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1807
1955
|
visibility: expandedConf.visibility,
|
|
1808
1956
|
sharedWithUsers: expandedConf.sharedWithUsers,
|
|
1809
1957
|
status: 'connecting',
|
|
1810
|
-
|
|
1958
|
+
recovery: existingServer?.recovery,
|
|
1959
|
+
error: existingServer?.recovery ? existingServer.error : null,
|
|
1811
1960
|
tools: [],
|
|
1812
1961
|
prompts: [],
|
|
1813
1962
|
resources: [],
|
|
@@ -1821,6 +1970,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1821
1970
|
const pendingAuth = expandedConf.oauth?.pendingAuthorization;
|
|
1822
1971
|
if (pendingAuth) {
|
|
1823
1972
|
serverInfo.status = 'oauth_required';
|
|
1973
|
+
serverInfo.recovery = undefined;
|
|
1824
1974
|
serverInfo.error = null;
|
|
1825
1975
|
serverInfo.oauth = {
|
|
1826
1976
|
authorizationUrl: pendingAuth.authorizationUrl,
|
|
@@ -1828,19 +1978,8 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1828
1978
|
};
|
|
1829
1979
|
}
|
|
1830
1980
|
nextServerInfos.push(serverInfo);
|
|
1831
|
-
//
|
|
1832
|
-
//
|
|
1833
|
-
// object `serverInfos` holds for this server - even when the reuse guard
|
|
1834
|
-
// "preserved" it, because preservation spreads the entry into a fresh
|
|
1835
|
-
// object. Writing the result into a replaced object leaves the server
|
|
1836
|
-
// stuck on 'connecting' for good while the log reports a successful
|
|
1837
|
-
// connect and a full tool list.
|
|
1838
|
-
//
|
|
1839
|
-
// While this pass is still the newest one, `serverInfo` is the entry that
|
|
1840
|
-
// will be published, so it is the right target even before the swap. Once
|
|
1841
|
-
// a later pass has run, resolve the live entry by name and only apply the
|
|
1842
|
-
// result if it still owns this client - which is exactly the case where
|
|
1843
|
-
// the later pass preserved this server rather than reconnecting it.
|
|
1981
|
+
// Before publication this pass owns the entry; after another pass, only
|
|
1982
|
+
// publish connection results if the live runtime still owns this client.
|
|
1844
1983
|
const liveServerInfo = () => {
|
|
1845
1984
|
if (generation === initGeneration)
|
|
1846
1985
|
return serverInfo;
|
|
@@ -1991,6 +2130,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1991
2130
|
info.status = 'oauth_required';
|
|
1992
2131
|
}
|
|
1993
2132
|
info.error = null;
|
|
2133
|
+
info.recovery = undefined;
|
|
1994
2134
|
}
|
|
1995
2135
|
else {
|
|
1996
2136
|
logger.error('Failed to connect client for server', {
|
|
@@ -2103,7 +2243,7 @@ export const getServersInfo = async (page, limit, user) => {
|
|
|
2103
2243
|
authorizationUrl: personal.authorizationUrl,
|
|
2104
2244
|
}
|
|
2105
2245
|
: undefined;
|
|
2106
|
-
const lease = await principalRuntimes.acquire(info.name, principal);
|
|
2246
|
+
const lease = await principalRuntimes.acquire(info.name, principal, true);
|
|
2107
2247
|
try {
|
|
2108
2248
|
return { ...lease.info, config: info.config, ...(ownOAuth ? { oauth: ownOAuth } : {}) };
|
|
2109
2249
|
}
|
|
@@ -2123,7 +2263,7 @@ export const getServersInfo = async (page, limit, user) => {
|
|
|
2123
2263
|
}));
|
|
2124
2264
|
const infos = presentedInfos
|
|
2125
2265
|
.filter((info) => requestedServerNames.has(info.name)) // Only include requested servers
|
|
2126
|
-
.map(({ name, version, packageVersion, latestVersion, updateAvailable, instructions, owner, visibility, status, tools, prompts, resources, createTime, error, oauth, }) => {
|
|
2266
|
+
.map(({ name, version, client, packageVersion, latestVersion, updateAvailable, instructions, owner, visibility, status, tools, prompts, resources, createTime, capabilityCache, error, recovery, oauth, }) => {
|
|
2127
2267
|
const serverConfig = allServers.find((server) => server.name === name);
|
|
2128
2268
|
const enabled = serverConfig ? serverConfig.enabled !== false : true;
|
|
2129
2269
|
const resolvedType = inferServerType(serverConfig);
|
|
@@ -2154,12 +2294,17 @@ export const getServersInfo = async (page, limit, user) => {
|
|
|
2154
2294
|
return {
|
|
2155
2295
|
name,
|
|
2156
2296
|
version,
|
|
2297
|
+
recovery: enabled ? recovery : undefined,
|
|
2298
|
+
protocolVersion: enabled && status === 'connected' && resolvedType !== 'openapi'
|
|
2299
|
+
? client?.getNegotiatedProtocolVersion?.()
|
|
2300
|
+
: undefined,
|
|
2157
2301
|
instructions,
|
|
2158
2302
|
owner,
|
|
2159
2303
|
visibility,
|
|
2160
2304
|
status,
|
|
2161
2305
|
error,
|
|
2162
2306
|
tools: toolsWithEnabled,
|
|
2307
|
+
...(capabilityCache ? { capabilityCache } : {}),
|
|
2163
2308
|
prompts: promptsWithEnabled,
|
|
2164
2309
|
resources: resourcesWithEnabled,
|
|
2165
2310
|
createTime,
|
|
@@ -2238,6 +2383,7 @@ export const reconnectServer = async (serverName) => {
|
|
|
2238
2383
|
}
|
|
2239
2384
|
// Close existing connection if any
|
|
2240
2385
|
if (serverInfo.client) {
|
|
2386
|
+
serverInfo.client.onclose = undefined;
|
|
2241
2387
|
try {
|
|
2242
2388
|
serverInfo.client.close();
|
|
2243
2389
|
}
|
|
@@ -2259,7 +2405,7 @@ export const reconnectServer = async (serverName) => {
|
|
|
2259
2405
|
}
|
|
2260
2406
|
// Reinitialize the server
|
|
2261
2407
|
await initializeClientsFromSettings(false, serverName);
|
|
2262
|
-
logger.log(`
|
|
2408
|
+
logger.log(`Scheduled connection initialization for server: ${serverName}`);
|
|
2263
2409
|
};
|
|
2264
2410
|
/**
|
|
2265
2411
|
* Resolve the installed package version for an npx/uvx stdio server and record
|
|
@@ -2519,6 +2665,9 @@ const clearIdleShutdown = (serverInfo) => {
|
|
|
2519
2665
|
};
|
|
2520
2666
|
const closeServerRuntime = (serverInfo) => {
|
|
2521
2667
|
clearIdleShutdown(serverInfo);
|
|
2668
|
+
if (serverInfo.client)
|
|
2669
|
+
serverInfo.client.onclose = undefined;
|
|
2670
|
+
serverInfo.recovery = undefined;
|
|
2522
2671
|
if (serverInfo.keepAliveIntervalId) {
|
|
2523
2672
|
clearInterval(serverInfo.keepAliveIntervalId);
|
|
2524
2673
|
serverInfo.keepAliveIntervalId = undefined;
|
|
@@ -2646,10 +2795,11 @@ const ensureServerReady = async (serverInfo) => {
|
|
|
2646
2795
|
if (expandedConf.type === 'openapi') {
|
|
2647
2796
|
return;
|
|
2648
2797
|
}
|
|
2798
|
+
await prepareCapabilitySnapshot(serverInfo, expandedConf);
|
|
2649
2799
|
const name = rawConfig.name;
|
|
2650
2800
|
logger.log(`[${name}] Cold-starting on-demand server…`);
|
|
2651
2801
|
const transport = await createTransportFromConfig(name, expandedConf);
|
|
2652
|
-
const client = createUpstreamMcpClient(name, () => serverInfo);
|
|
2802
|
+
const client = createUpstreamMcpClient(name, () => serverInfo, expandedConf);
|
|
2653
2803
|
const serverRequestOptions = expandedConf.options || {};
|
|
2654
2804
|
const defaultRequestTimeout = Number(process.env.DEFAULT_REQUEST_TIMEOUT) || 60000;
|
|
2655
2805
|
const requestOptions = {
|
|
@@ -2687,6 +2837,11 @@ const ensureServerReady = async (serverInfo) => {
|
|
|
2687
2837
|
});
|
|
2688
2838
|
}
|
|
2689
2839
|
}
|
|
2840
|
+
else {
|
|
2841
|
+
updateServerToolsCache(serverInfo, []);
|
|
2842
|
+
broadcastToolListChanged();
|
|
2843
|
+
}
|
|
2844
|
+
await flushCapabilitySnapshot(serverInfo);
|
|
2690
2845
|
if (capabilities?.prompts) {
|
|
2691
2846
|
try {
|
|
2692
2847
|
const prompts = await client.listPrompts({}, requestOptions);
|
|
@@ -2750,6 +2905,7 @@ const primeOnDemandServers = (gate) => {
|
|
|
2750
2905
|
!hasPersonalAuthentication(si.config) &&
|
|
2751
2906
|
isStdioServer(si.config) &&
|
|
2752
2907
|
si.tools.length === 0 &&
|
|
2908
|
+
si.capabilityCache?.state !== 'fresh' &&
|
|
2753
2909
|
si.enabled !== false);
|
|
2754
2910
|
if (targets.length === 0)
|
|
2755
2911
|
return Promise.resolve();
|
|
@@ -2763,7 +2919,10 @@ const primeOnDemandServers = (gate) => {
|
|
|
2763
2919
|
await run(() => ensureServerReady(si),
|
|
2764
2920
|
// A reload may replace this runtime or finish its discovery while it
|
|
2765
2921
|
// waits for a slot. Neither needs another child from the old queue.
|
|
2766
|
-
() => serverInfos.includes(si) &&
|
|
2922
|
+
() => serverInfos.includes(si) &&
|
|
2923
|
+
si.enabled !== false &&
|
|
2924
|
+
si.tools.length === 0 &&
|
|
2925
|
+
si.capabilityCache?.state !== 'fresh');
|
|
2767
2926
|
// Sleep the child unless a tool call is using it; keep the cached tool list.
|
|
2768
2927
|
shutdownOnDemandServer(si);
|
|
2769
2928
|
logger.log('On-demand server tool cache primed');
|
|
@@ -3046,7 +3205,7 @@ const assertToolAvailableForRoute = (tool, appsRouteContext) => {
|
|
|
3046
3205
|
throw new Error(`Tool '${tool.name}' is only available to MCP Apps`);
|
|
3047
3206
|
}
|
|
3048
3207
|
};
|
|
3049
|
-
const resolveToolInGroup = async (group, toolName) => {
|
|
3208
|
+
const resolveToolInGroup = async (group, toolName, appsRouteContext) => {
|
|
3050
3209
|
const lookupGroup = getGroupLookupName(group);
|
|
3051
3210
|
if (!lookupGroup) {
|
|
3052
3211
|
return undefined;
|
|
@@ -3058,22 +3217,30 @@ const resolveToolInGroup = async (group, toolName) => {
|
|
|
3058
3217
|
const allowRawName = !isSmartRoutingGroup(group) &&
|
|
3059
3218
|
serverConfigsByName.size <= 1 &&
|
|
3060
3219
|
filteredServerInfos.length === 1;
|
|
3220
|
+
// Validate the full visible catalog before routing, even if the client skips tools/list.
|
|
3221
|
+
const hasOriginalToolNames = [...serverConfigsByName.values()].some((config) => config.preserveToolNames);
|
|
3222
|
+
if (hasOriginalToolNames) {
|
|
3223
|
+
await listGroupTools(lookupGroup, filteredServerInfos, serverConfigsByName, appsRouteContext);
|
|
3224
|
+
}
|
|
3061
3225
|
for (const serverInfo of filteredServerInfos) {
|
|
3062
3226
|
// An idle on-demand server is selected here and woken by the caller (#1029)
|
|
3063
3227
|
if (!canServeToolRequests(serverInfo)) {
|
|
3064
3228
|
continue;
|
|
3065
3229
|
}
|
|
3066
3230
|
const serverConfig = serverConfigsByName.get(serverInfo.name);
|
|
3067
|
-
const internalToolName =
|
|
3231
|
+
const internalToolName = serverConfig?.preserveToolNames
|
|
3232
|
+
? `${serverInfo.name}${getNameSeparator()}${toolName}`
|
|
3233
|
+
: resolveNameFromGroup(toolName, serverInfo.name, serverConfig);
|
|
3068
3234
|
const tool = findToolOnServer(serverInfo, internalToolName, allowRawName, serverConfigsByName.size === 0);
|
|
3069
|
-
if (!tool) {
|
|
3235
|
+
if (!tool || (hasOriginalToolNames && !appsRouteContext.enabled && isAppOnlyTool(tool))) {
|
|
3070
3236
|
continue;
|
|
3071
3237
|
}
|
|
3072
3238
|
const filteredTools = await filterToolsByGroup(lookupGroup, serverInfo.name, [tool], serverConfig);
|
|
3073
3239
|
if (filteredTools.length === 0) {
|
|
3074
3240
|
continue;
|
|
3075
3241
|
}
|
|
3076
|
-
|
|
3242
|
+
// Retain the resolved identity when callers revalidate discovery after waking.
|
|
3243
|
+
return { serverInfo, toolName: tool.name, tool };
|
|
3077
3244
|
}
|
|
3078
3245
|
return undefined;
|
|
3079
3246
|
};
|
|
@@ -3127,7 +3294,7 @@ const projectToolForDownstream = (serverName, tool, appsRouteContext, serverConf
|
|
|
3127
3294
|
// Group and aggregate routes keep qualified names, including single-member groups.
|
|
3128
3295
|
return {
|
|
3129
3296
|
...projectedTool,
|
|
3130
|
-
name:
|
|
3297
|
+
name: projectGroupToolName(projectedTool.name, serverName, serverConfig, getNameSeparator()),
|
|
3131
3298
|
};
|
|
3132
3299
|
};
|
|
3133
3300
|
const listGroupTools = async (group, filteredServerInfos, serverConfigsByName, appsRouteContext) => {
|
|
@@ -3135,7 +3302,10 @@ const listGroupTools = async (group, filteredServerInfos, serverConfigsByName, a
|
|
|
3135
3302
|
// so this list reflects the freshly cached tools instead of returning empty.
|
|
3136
3303
|
// No wake is triggered from list itself; the prime handles that. See #1029.
|
|
3137
3304
|
await Promise.allSettled(filteredServerInfos
|
|
3138
|
-
.filter((si) => si.config?.startOnDemand === true &&
|
|
3305
|
+
.filter((si) => si.config?.startOnDemand === true &&
|
|
3306
|
+
si.tools.length === 0 &&
|
|
3307
|
+
si.capabilityCache?.state !== 'fresh' &&
|
|
3308
|
+
si.spawningPromise)
|
|
3139
3309
|
.map((si) => si.spawningPromise));
|
|
3140
3310
|
const useRawName = !!group && !isSmartRoutingGroup(group) && !(await findRouteGroup(group));
|
|
3141
3311
|
const allTools = [];
|
|
@@ -3162,6 +3332,9 @@ const listGroupTools = async (group, filteredServerInfos, serverConfigsByName, a
|
|
|
3162
3332
|
}));
|
|
3163
3333
|
}
|
|
3164
3334
|
}
|
|
3335
|
+
if ([...serverConfigsByName.values()].some((config) => config.preserveToolNames)) {
|
|
3336
|
+
assertValidGroupToolNames(allTools);
|
|
3337
|
+
}
|
|
3165
3338
|
return allTools;
|
|
3166
3339
|
};
|
|
3167
3340
|
// Names the call handler always routes to a meta-tool on a Smart Routing session,
|
|
@@ -3351,7 +3524,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3351
3524
|
targetServerInfo = getVisibleServerByName(extra.server);
|
|
3352
3525
|
}
|
|
3353
3526
|
else if (getGroupLookupName(group)) {
|
|
3354
|
-
const groupTool = await resolveToolInGroup(group, toolName);
|
|
3527
|
+
const groupTool = await resolveToolInGroup(group, toolName, appsRouteContext);
|
|
3355
3528
|
if (groupTool) {
|
|
3356
3529
|
targetServerInfo = groupTool.serverInfo;
|
|
3357
3530
|
targetToolName = groupTool.toolName;
|
|
@@ -3376,6 +3549,8 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3376
3549
|
if (targetServerInfo.config?.startOnDemand && targetServerInfo.status !== 'connected') {
|
|
3377
3550
|
await ensureServerReady(targetServerInfo);
|
|
3378
3551
|
// Re-read status from the mutated serverInfo after async spawn
|
|
3552
|
+
// Routing may have captured a tool from the persisted directory before wake.
|
|
3553
|
+
targetTool = undefined;
|
|
3379
3554
|
const freshStatus = targetServerInfo.status;
|
|
3380
3555
|
if (freshStatus !== 'connected') {
|
|
3381
3556
|
throw new Error(`Failed to start on-demand server '${targetServerInfo.name}' — check server logs`);
|
|
@@ -3549,13 +3724,13 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3549
3724
|
// Apps support; keep legacy raw aggregate calls compatible.
|
|
3550
3725
|
const singleServerAppsRoute = !getGroupLookupName(group) && !!appsRouteContext.serverInfo;
|
|
3551
3726
|
const groupTool = !singleServerAppsRoute && lookupGroup
|
|
3552
|
-
? await resolveToolInGroup(group, request.params.name)
|
|
3727
|
+
? await resolveToolInGroup(group, request.params.name, appsRouteContext)
|
|
3553
3728
|
: undefined;
|
|
3554
3729
|
const serverInfo = singleServerAppsRoute
|
|
3555
3730
|
? appsRouteContext.serverInfo
|
|
3556
3731
|
: (groupTool?.serverInfo ?? (lookupGroup ? undefined : getServerByTool(request.params.name)));
|
|
3557
3732
|
const routeToolName = groupTool?.toolName ?? request.params.name;
|
|
3558
|
-
|
|
3733
|
+
let tool = groupTool?.tool ??
|
|
3559
3734
|
(serverInfo ? findToolOnServer(serverInfo, routeToolName, singleServerAppsRoute) : undefined);
|
|
3560
3735
|
if (!serverInfo || !tool) {
|
|
3561
3736
|
throw new ToolUnavailableError(`Tool not available: ${request.params.name}`, classifyUnavailableReason(request.params.name));
|
|
@@ -3568,6 +3743,10 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3568
3743
|
releaseOnDemandToolCall = beginOnDemandToolCall(serverInfo);
|
|
3569
3744
|
if (serverInfo.config?.startOnDemand && serverInfo.status !== 'connected') {
|
|
3570
3745
|
await ensureServerReady(serverInfo);
|
|
3746
|
+
tool = findToolOnServer(serverInfo, routeToolName, singleServerAppsRoute);
|
|
3747
|
+
if (!tool)
|
|
3748
|
+
throw new ToolUnavailableError(`Tool not available: ${request.params.name}`, 'tool-not-found');
|
|
3749
|
+
assertToolAvailableForRoute(tool, appsRouteContext);
|
|
3571
3750
|
}
|
|
3572
3751
|
serverInfo.lastUsedAt = Date.now();
|
|
3573
3752
|
// Handle OpenAPI servers differently
|
|
@@ -4118,10 +4297,41 @@ const handleReadResourceRequestImpl = async (request, extra) => {
|
|
|
4118
4297
|
};
|
|
4119
4298
|
}
|
|
4120
4299
|
};
|
|
4300
|
+
// Preserve credential-safe errors for personal clients in every discovery scope.
|
|
4301
|
+
const personalClientMethods = new Set([
|
|
4302
|
+
'callTool',
|
|
4303
|
+
'getPrompt',
|
|
4304
|
+
'readResource',
|
|
4305
|
+
'listTools',
|
|
4306
|
+
'listPrompts',
|
|
4307
|
+
'listResources',
|
|
4308
|
+
'listResourceTemplates',
|
|
4309
|
+
]);
|
|
4310
|
+
const protectPersonalClient = (client, info, resolvedConfig) => new Proxy(client, {
|
|
4311
|
+
get(target, property) {
|
|
4312
|
+
const value = Reflect.get(target, property);
|
|
4313
|
+
if (typeof value !== 'function')
|
|
4314
|
+
return value;
|
|
4315
|
+
if (!personalClientMethods.has(String(property)))
|
|
4316
|
+
return value.bind(target);
|
|
4317
|
+
return async (...args) => {
|
|
4318
|
+
try {
|
|
4319
|
+
return await value.apply(target, args);
|
|
4320
|
+
}
|
|
4321
|
+
catch (error) {
|
|
4322
|
+
if (isUpstreamConnectionFailure(error))
|
|
4323
|
+
info.status = 'disconnected';
|
|
4324
|
+
const failure = createUpstreamRequestError(info.name, String(property), error, resolvedConfig);
|
|
4325
|
+
logger.error(failure.message);
|
|
4326
|
+
throw failure;
|
|
4327
|
+
}
|
|
4328
|
+
};
|
|
4329
|
+
},
|
|
4330
|
+
});
|
|
4121
4331
|
// Personal runtimes never enter serverInfos or config exports. Their tool list
|
|
4122
4332
|
// is added to the rows under the server name, the index the reindex endpoint
|
|
4123
4333
|
// rebuilds from every binding.
|
|
4124
|
-
const createPrincipalRuntime = async (name, resolvedConfig) => {
|
|
4334
|
+
const createPrincipalRuntime = async (name, resolvedConfig, scope, toolsOnly = false) => {
|
|
4125
4335
|
const info = {
|
|
4126
4336
|
name,
|
|
4127
4337
|
owner: resolvedConfig.owner,
|
|
@@ -4137,41 +4347,22 @@ const createPrincipalRuntime = async (name, resolvedConfig) => {
|
|
|
4137
4347
|
resources: [],
|
|
4138
4348
|
createTime: Date.now(),
|
|
4139
4349
|
};
|
|
4140
|
-
const
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
const value = Reflect.get(target, property);
|
|
4152
|
-
if (typeof value !== 'function')
|
|
4153
|
-
return value;
|
|
4154
|
-
if (!safeMethods.has(String(property)))
|
|
4155
|
-
return value.bind(target);
|
|
4156
|
-
return async (...args) => {
|
|
4157
|
-
try {
|
|
4158
|
-
return await value.apply(target, args);
|
|
4159
|
-
}
|
|
4160
|
-
catch (error) {
|
|
4161
|
-
if (isUpstreamConnectionFailure(error))
|
|
4162
|
-
info.status = 'disconnected';
|
|
4163
|
-
const failure = createUpstreamRequestError(name, String(property), error, resolvedConfig);
|
|
4164
|
-
logger.error(failure.message);
|
|
4165
|
-
throw failure;
|
|
4166
|
-
}
|
|
4167
|
-
};
|
|
4168
|
-
},
|
|
4169
|
-
});
|
|
4350
|
+
const cachedTools = await prepareCapabilitySnapshot(info, resolvedConfig, scope);
|
|
4351
|
+
if (toolsOnly) {
|
|
4352
|
+
if (cachedTools === undefined)
|
|
4353
|
+
return undefined;
|
|
4354
|
+
info.tools = cachedTools.map((tool) => normalizeToolForCache(name, tool));
|
|
4355
|
+
void syncCredentialServerToolEmbeddings(name, info.tools, { partial: true }).catch(() => {
|
|
4356
|
+
logger.warn('[EMBED_SYNC_ERROR] Failed to sync personal tool embeddings');
|
|
4357
|
+
});
|
|
4358
|
+
info.status = 'disconnected';
|
|
4359
|
+
return info;
|
|
4360
|
+
}
|
|
4170
4361
|
try {
|
|
4171
4362
|
if (resolvedConfig.type === 'openapi') {
|
|
4172
4363
|
const client = new OpenAPIClient(resolvedConfig);
|
|
4173
4364
|
await client.initialize();
|
|
4174
|
-
info.openApiClient =
|
|
4365
|
+
info.openApiClient = protectPersonalClient(client, info, resolvedConfig);
|
|
4175
4366
|
info.tools = client.getTools().map((tool) => ({
|
|
4176
4367
|
...tool,
|
|
4177
4368
|
name: `${name}${getNameSeparator()}${tool.name}`,
|
|
@@ -4181,17 +4372,23 @@ const createPrincipalRuntime = async (name, resolvedConfig) => {
|
|
|
4181
4372
|
else {
|
|
4182
4373
|
const transport = await createTransportFromConfig(name, resolvedConfig);
|
|
4183
4374
|
const client = new LegacyMcpClient({ name: `mcp-client-${name}`, version: '1.0.0' }, {
|
|
4375
|
+
versionNegotiation: { mode: resolveUpstreamProtocolMode(resolvedConfig) },
|
|
4184
4376
|
capabilities: MCP_APPS_CAPABILITIES,
|
|
4185
4377
|
jsonSchemaValidator: new ResilientJsonSchemaValidator(),
|
|
4186
4378
|
});
|
|
4187
4379
|
info.transport = transport;
|
|
4188
|
-
info.client =
|
|
4380
|
+
info.client = protectPersonalClient(client, info, resolvedConfig);
|
|
4189
4381
|
await client.connect(transport, resolvedConfig.options || {});
|
|
4190
4382
|
const capabilities = client.getServerCapabilities();
|
|
4191
4383
|
if (capabilities?.tools) {
|
|
4192
4384
|
const { tools } = await client.listTools({}, resolvedConfig.options);
|
|
4193
4385
|
info.tools = preserveListFreshness(tools, tools.map((tool) => normalizeToolForCache(name, tool)));
|
|
4386
|
+
recordCapabilitySnapshot(info, tools);
|
|
4194
4387
|
}
|
|
4388
|
+
else {
|
|
4389
|
+
recordCapabilitySnapshot(info, []);
|
|
4390
|
+
}
|
|
4391
|
+
await flushCapabilitySnapshot(info);
|
|
4195
4392
|
if (capabilities?.prompts) {
|
|
4196
4393
|
const { prompts } = await client.listPrompts({}, resolvedConfig.options);
|
|
4197
4394
|
info.prompts = preserveListFreshness(prompts, prompts.map((prompt) => normalizePromptForCache(name, prompt)));
|
|
@@ -4226,7 +4423,7 @@ const createPrincipalRuntime = async (name, resolvedConfig) => {
|
|
|
4226
4423
|
throw failure;
|
|
4227
4424
|
}
|
|
4228
4425
|
};
|
|
4229
|
-
const principalRuntimes = new PrincipalRuntimeService(createPrincipalRuntime, closeServerRuntime);
|
|
4426
|
+
const principalRuntimes = new PrincipalRuntimeService(async (name, config, scope) => (await createPrincipalRuntime(name, config, scope)), closeServerRuntime, (name, config, scope) => createPrincipalRuntime(name, config, scope, true));
|
|
4230
4427
|
/**
|
|
4231
4428
|
* Resolve the tools of a personal-credential server on behalf of an explicit
|
|
4232
4429
|
* principal, outside any request context.
|
|
@@ -4243,12 +4440,112 @@ const principalRuntimes = new PrincipalRuntimeService(createPrincipalRuntime, cl
|
|
|
4243
4440
|
export const getServerToolsForPrincipal = async (serverName, principal) => {
|
|
4244
4441
|
const lease = await principalRuntimes.acquire(serverName, principal);
|
|
4245
4442
|
try {
|
|
4443
|
+
await refreshModernServerLists(lease.info, ['tools']);
|
|
4246
4444
|
return lease.info.tools ?? [];
|
|
4247
4445
|
}
|
|
4248
4446
|
finally {
|
|
4249
4447
|
lease.release();
|
|
4250
4448
|
}
|
|
4251
4449
|
};
|
|
4450
|
+
const withModernLists = (handler, fields) => async (request, extra) => {
|
|
4451
|
+
if (!(principalServerScope.getStore() ?? serverInfos).some((info) => info.client?.getProtocolEra?.() === 'modern')) {
|
|
4452
|
+
return handler(request, extra);
|
|
4453
|
+
}
|
|
4454
|
+
// Built-in content must not depend on an unrelated upstream being healthy.
|
|
4455
|
+
if (fields[0] === 'prompts' && request.method === 'prompts/get' && !extra?.server) {
|
|
4456
|
+
const builtin = await getBuiltinPromptDao().findByName(request.params?.name);
|
|
4457
|
+
if (builtin && builtin.enabled !== false)
|
|
4458
|
+
return handler(request, extra);
|
|
4459
|
+
}
|
|
4460
|
+
if (fields[0] === 'resources' && request.method === 'resources/read') {
|
|
4461
|
+
const builtin = await getBuiltinResourceDao().findByUri(request.params?.uri);
|
|
4462
|
+
if (builtin && builtin.enabled !== false)
|
|
4463
|
+
return handler(request, extra);
|
|
4464
|
+
}
|
|
4465
|
+
const { filteredServerInfos, serverConfigsByName } = await getFilteredServerInfosForGroup(getGroupLookupName(getMcpRequestGroup(extra)));
|
|
4466
|
+
const requestedName = ['call_tool', 'describe_tool'].includes(request?.params?.name)
|
|
4467
|
+
? request.params.arguments?.toolName
|
|
4468
|
+
: request?.params?.name;
|
|
4469
|
+
let selected = filteredServerInfos;
|
|
4470
|
+
if (extra?.server) {
|
|
4471
|
+
selected = selected.filter((info) => info.name === extra.server);
|
|
4472
|
+
}
|
|
4473
|
+
else if (typeof requestedName === 'string' &&
|
|
4474
|
+
!(fields[0] === 'tools' &&
|
|
4475
|
+
[...serverConfigsByName.values()].some((config) => config.preserveToolNames))) {
|
|
4476
|
+
// Match the routing resolver's longest exposed prefix, including aliases.
|
|
4477
|
+
const targets = selected
|
|
4478
|
+
.map((info) => ({
|
|
4479
|
+
info,
|
|
4480
|
+
prefix: `${getExposedServerName(info.name, serverConfigsByName.get(info.name))}${getNameSeparator()}`,
|
|
4481
|
+
}))
|
|
4482
|
+
.filter(({ prefix }) => requestedName.startsWith(prefix))
|
|
4483
|
+
.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
4484
|
+
if (targets.length)
|
|
4485
|
+
selected = [targets[0].info];
|
|
4486
|
+
}
|
|
4487
|
+
const scoped = new Map((principalServerScope.getStore() ?? serverInfos).map((info) => [info.name, info]));
|
|
4488
|
+
const requestClients = [];
|
|
4489
|
+
try {
|
|
4490
|
+
await Promise.all(selected.map(async (info) => {
|
|
4491
|
+
try {
|
|
4492
|
+
let discoveryInfo = info;
|
|
4493
|
+
const serverConfig = info.config;
|
|
4494
|
+
if (info.status === 'connected' &&
|
|
4495
|
+
info.client?.getProtocolEra?.() === 'modern' &&
|
|
4496
|
+
serverConfig?.passthroughHeaders?.some(isSafePassthroughHeader)) {
|
|
4497
|
+
// Reuse neither snapshots nor pending work across forwarded identities.
|
|
4498
|
+
// A disposable modern client also isolates SDK tool definitions and read caches.
|
|
4499
|
+
const transport = await createTransportFromConfig(info.name, serverConfig);
|
|
4500
|
+
const client = new LegacyMcpClient({ name: `mcp-client-${info.name}`, version: '1.0.0' }, {
|
|
4501
|
+
versionNegotiation: {
|
|
4502
|
+
mode: { pin: info.client.getNegotiatedProtocolVersion() },
|
|
4503
|
+
},
|
|
4504
|
+
capabilities: MCP_APPS_CAPABILITIES,
|
|
4505
|
+
jsonSchemaValidator: new ResilientJsonSchemaValidator(),
|
|
4506
|
+
});
|
|
4507
|
+
discoveryInfo = { ...info, client, transport, tools: [], prompts: [], resources: [] };
|
|
4508
|
+
if (hasPersonalAuthentication(serverConfig)) {
|
|
4509
|
+
discoveryInfo.client = protectPersonalClient(client, discoveryInfo, serverConfig);
|
|
4510
|
+
}
|
|
4511
|
+
requestClients.push(discoveryInfo.client);
|
|
4512
|
+
requestDiscoveryInfos.add(discoveryInfo);
|
|
4513
|
+
scoped.set(info.name, discoveryInfo);
|
|
4514
|
+
await connectClientWithDiagnostics(client, transport, info.options || {});
|
|
4515
|
+
}
|
|
4516
|
+
await refreshModernServerLists(discoveryInfo, fields);
|
|
4517
|
+
}
|
|
4518
|
+
catch {
|
|
4519
|
+
// Exclude only this upstream for this request. Keep its shared state
|
|
4520
|
+
// intact so later requests retry; disconnected projections force TTL zero.
|
|
4521
|
+
scoped.set(info.name, {
|
|
4522
|
+
...info,
|
|
4523
|
+
status: 'disconnected',
|
|
4524
|
+
client: undefined,
|
|
4525
|
+
tools: [],
|
|
4526
|
+
prompts: [],
|
|
4527
|
+
resources: [],
|
|
4528
|
+
});
|
|
4529
|
+
logger.warn('Failed to refresh upstream discovery for request', {
|
|
4530
|
+
serverName: info.name,
|
|
4531
|
+
});
|
|
4532
|
+
}
|
|
4533
|
+
}));
|
|
4534
|
+
return await principalServerScope.run([...scoped.values()], () => handler(request, extra));
|
|
4535
|
+
}
|
|
4536
|
+
finally {
|
|
4537
|
+
await Promise.all([
|
|
4538
|
+
...new Set([
|
|
4539
|
+
...requestClients,
|
|
4540
|
+
...[...scoped.values()]
|
|
4541
|
+
.filter((info) => requestDiscoveryInfos.has(info))
|
|
4542
|
+
.flatMap((info) => (info.client ? [info.client] : [])),
|
|
4543
|
+
]),
|
|
4544
|
+
].map((client) => client.close().catch(() => {
|
|
4545
|
+
logger.warn('Failed to close request discovery client');
|
|
4546
|
+
})));
|
|
4547
|
+
}
|
|
4548
|
+
};
|
|
4252
4549
|
const withPrincipalServers = (handler, operation) => async (request, extra) => {
|
|
4253
4550
|
if (!serverInfos.some((info) => hasPersonalAuthentication(info.config)))
|
|
4254
4551
|
return handler(request, extra);
|
|
@@ -4274,7 +4571,10 @@ const withPrincipalServers = (handler, operation) => async (request, extra) => {
|
|
|
4274
4571
|
if (extra?.server && !target && (operation === 'tool' || operation === 'prompt')) {
|
|
4275
4572
|
throw new ToolUnavailableError(`Tool not available: ${requestedName}`, 'server-or-name-not-found');
|
|
4276
4573
|
}
|
|
4277
|
-
if (!extra?.server &&
|
|
4574
|
+
if (!extra?.server &&
|
|
4575
|
+
typeof requestedName === 'string' &&
|
|
4576
|
+
!(operation === 'tool' &&
|
|
4577
|
+
[...serverConfigsByName.values()].some((config) => config.preserveToolNames))) {
|
|
4278
4578
|
let longestPrefix = 0;
|
|
4279
4579
|
for (const info of candidates) {
|
|
4280
4580
|
const exposedName = getExposedServerName(info.name, serverConfigsByName.get(info.name));
|
|
@@ -4303,7 +4603,7 @@ const withPrincipalServers = (handler, operation) => async (request, extra) => {
|
|
|
4303
4603
|
throw hasPersonalOAuth(info.config)
|
|
4304
4604
|
? personalAuthorizationRequired(info.name)
|
|
4305
4605
|
: missingCredentialError(info.name);
|
|
4306
|
-
const lease = await principalRuntimes.acquire(info.name, user);
|
|
4606
|
+
const lease = await principalRuntimes.acquire(info.name, user, operation === 'tools-list');
|
|
4307
4607
|
releases.push(lease.release);
|
|
4308
4608
|
scoped.set(info.name, lease.info);
|
|
4309
4609
|
}
|
|
@@ -4314,7 +4614,10 @@ const withPrincipalServers = (handler, operation) => async (request, extra) => {
|
|
|
4314
4614
|
}
|
|
4315
4615
|
}));
|
|
4316
4616
|
const requiredTarget = target ||
|
|
4317
|
-
(candidates.length === 1 &&
|
|
4617
|
+
(candidates.length === 1 &&
|
|
4618
|
+
operation !== 'list' &&
|
|
4619
|
+
operation !== 'tools-list' &&
|
|
4620
|
+
requestedName !== 'search_tools'
|
|
4318
4621
|
? templated[0]
|
|
4319
4622
|
: undefined);
|
|
4320
4623
|
const failure = requiredTarget ? failures.get(requiredTarget.name) : undefined;
|
|
@@ -4443,14 +4746,16 @@ const withCacheHints = (handler, passthroughTtl = false, listField) => async (re
|
|
|
4443
4746
|
// and capabilities. Shared caches must not reuse the gateway result across users.
|
|
4444
4747
|
return { ...result, ttlMs, cacheScope: 'private' };
|
|
4445
4748
|
};
|
|
4446
|
-
export const handleListToolsRequest = withPrincipalServers(withCacheHints(handleListToolsRequestImpl, false, 'tools'), 'list');
|
|
4447
|
-
const callToolWithPrincipal = withPrincipalServers(handleCallToolRequestImpl, 'tool');
|
|
4749
|
+
export const handleListToolsRequest = withPrincipalServers(withModernLists(withCacheHints(handleListToolsRequestImpl, false, 'tools'), ['tools']), 'tools-list');
|
|
4750
|
+
const callToolWithPrincipal = withPrincipalServers(withModernLists(handleCallToolRequestImpl, ['tools']), 'tool');
|
|
4448
4751
|
export const handleCallToolRequest = (request, extra) => observeToolCall(() => callToolWithPrincipal(request, extra));
|
|
4449
|
-
export const handleGetPromptRequest = withPrincipalServers(handleGetPromptRequestImpl, 'prompt');
|
|
4450
|
-
export const handleListPromptsRequest = withPrincipalServers(withCacheHints(handleListPromptsRequestImpl, false, 'prompts'), 'list');
|
|
4451
|
-
export const handleListResourcesRequest = withPrincipalServers(withCacheHints(handleListResourcesRequestImpl, false, 'resources'),
|
|
4752
|
+
export const handleGetPromptRequest = withPrincipalServers(withModernLists(handleGetPromptRequestImpl, ['prompts']), 'prompt');
|
|
4753
|
+
export const handleListPromptsRequest = withPrincipalServers(withModernLists(withCacheHints(handleListPromptsRequestImpl, false, 'prompts'), ['prompts']), 'list');
|
|
4754
|
+
export const handleListResourcesRequest = withPrincipalServers(withModernLists(withCacheHints(handleListResourcesRequestImpl, false, 'resources'), [
|
|
4755
|
+
'resources',
|
|
4756
|
+
]), 'list');
|
|
4452
4757
|
export const handleListResourceTemplatesRequest = withPrincipalServers(withCacheHints(handleListResourceTemplatesRequestImpl, false, 'resourceTemplates'), 'list');
|
|
4453
|
-
export const handleReadResourceRequest = withPrincipalServers(withCacheHints(handleReadResourceRequestImpl, true), 'resource');
|
|
4758
|
+
export const handleReadResourceRequest = withPrincipalServers(withModernLists(withCacheHints(handleReadResourceRequestImpl, true), ['resources']), 'resource');
|
|
4454
4759
|
export const createMcpServer = (name, version, options) => {
|
|
4455
4760
|
const normalizedOptions = typeof options === 'string' ? { group: options } : (options ?? {});
|
|
4456
4761
|
// Determine server name based on routing type
|