@samanhappy/mcphub 1.1.10 → 1.1.12
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/README.md +4 -0
- package/README.zh.md +4 -0
- package/dist/cli/commands/keys.js +2 -2
- package/dist/cli/commands/keys.js.map +1 -1
- package/dist/clients/legacyMcpClient.js +10 -2
- package/dist/clients/legacyMcpClient.js.map +1 -1
- package/dist/controllers/bearerKeyController.js +5 -4
- package/dist/controllers/bearerKeyController.js.map +1 -1
- package/dist/controllers/configController.js +1 -1
- package/dist/controllers/configController.js.map +1 -1
- package/dist/controllers/groupController.js +19 -0
- package/dist/controllers/groupController.js.map +1 -1
- package/dist/controllers/serverController.js +5 -2
- package/dist/controllers/serverController.js.map +1 -1
- package/dist/dao/BearerKeyDao.js +34 -7
- package/dist/dao/BearerKeyDao.js.map +1 -1
- package/dist/dao/BearerKeyDaoDbImpl.js +18 -3
- package/dist/dao/BearerKeyDaoDbImpl.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/BearerKey.js +10 -2
- package/dist/db/entities/BearerKey.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/db/repositories/BearerKeyRepository.js +20 -3
- package/dist/db/repositories/BearerKeyRepository.js.map +1 -1
- package/dist/middlewares/auth.js +2 -2
- package/dist/middlewares/auth.js.map +1 -1
- package/dist/server.js +11 -1
- package/dist/server.js.map +1 -1
- package/dist/services/capabilitySnapshotService.js +106 -0
- package/dist/services/capabilitySnapshotService.js.map +1 -0
- package/dist/services/credentialBindingService.js +4 -0
- package/dist/services/credentialBindingService.js.map +1 -1
- 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 +405 -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/sseService.js +3 -3
- package/dist/services/sseService.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/bearerKeyToken.js +27 -0
- package/dist/utils/bearerKeyToken.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 +63 -5
- 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-DDa3yAck.js → ActivityPage-DWA-okla.js} +2 -2
- package/frontend/dist/assets/{ActivityPage-DDa3yAck.js.map → ActivityPage-DWA-okla.js.map} +1 -1
- package/frontend/dist/assets/{CopyClientConfigDialog-DPQOggV-.js → CopyClientConfigDialog-xeHg7JJj.js} +2 -2
- package/frontend/dist/assets/{CopyClientConfigDialog-DPQOggV-.js.map → CopyClientConfigDialog-xeHg7JJj.js.map} +1 -1
- package/frontend/dist/assets/{CredentialsPage-DmxY-iXb.js → CredentialsPage-D8mMIH5f.js} +2 -2
- package/frontend/dist/assets/{CredentialsPage-DmxY-iXb.js.map → CredentialsPage-D8mMIH5f.js.map} +1 -1
- package/frontend/dist/assets/{Dashboard-DCvveQBj.js → Dashboard-Diql_Btr.js} +2 -2
- package/frontend/dist/assets/{Dashboard-DCvveQBj.js.map → Dashboard-Diql_Btr.js.map} +1 -1
- package/frontend/dist/assets/{EndpointCopy-DRWz8guH.js → EndpointCopy-0d-dK6uJ.js} +2 -2
- package/frontend/dist/assets/{EndpointCopy-DRWz8guH.js.map → EndpointCopy-0d-dK6uJ.js.map} +1 -1
- package/frontend/dist/assets/GroupsPage-gZ0nnzCn.js +33 -0
- package/frontend/dist/assets/GroupsPage-gZ0nnzCn.js.map +1 -0
- package/frontend/dist/assets/{LoginPage-BVBf8nr6.js → LoginPage-BRyFcGQ5.js} +2 -2
- package/frontend/dist/assets/{LoginPage-BVBf8nr6.js.map → LoginPage-BRyFcGQ5.js.map} +1 -1
- package/frontend/dist/assets/{LogsPage-DbsmNJcA.js → LogsPage-C7erxObE.js} +2 -2
- package/frontend/dist/assets/{LogsPage-DbsmNJcA.js.map → LogsPage-C7erxObE.js.map} +1 -1
- package/frontend/dist/assets/{MarketPage-BOggDLJb.js → MarketPage-BZHQQB57.js} +2 -2
- package/frontend/dist/assets/{MarketPage-BOggDLJb.js.map → MarketPage-BZHQQB57.js.map} +1 -1
- package/frontend/dist/assets/{OAuthConsentPage-VON__2op.js → OAuthConsentPage-Czhwhcx8.js} +2 -2
- package/frontend/dist/assets/{OAuthConsentPage-VON__2op.js.map → OAuthConsentPage-Czhwhcx8.js.map} +1 -1
- package/frontend/dist/assets/{PromptsPage-BNkvIDZU.js → PromptsPage-D0yHQbns.js} +2 -2
- package/frontend/dist/assets/{PromptsPage-BNkvIDZU.js.map → PromptsPage-D0yHQbns.js.map} +1 -1
- package/frontend/dist/assets/{ResourcesPage-4WBvamN_.js → ResourcesPage-BFoqAAq0.js} +2 -2
- package/frontend/dist/assets/{ResourcesPage-4WBvamN_.js.map → ResourcesPage-BFoqAAq0.js.map} +1 -1
- package/frontend/dist/assets/{ServersPage-C7000Szj.js → ServersPage-CMG1tNig.js} +9 -9
- package/frontend/dist/assets/ServersPage-CMG1tNig.js.map +1 -0
- package/frontend/dist/assets/{SettingsPage-YX6dJjrM.js → SettingsPage-C6GAVHlw.js} +2 -2
- package/frontend/dist/assets/{SettingsPage-YX6dJjrM.js.map → SettingsPage-C6GAVHlw.js.map} +1 -1
- package/frontend/dist/assets/{StatusDot-mgJveZ2h.js → StatusDot-BniiEZ-W.js} +2 -2
- package/frontend/dist/assets/{StatusDot-mgJveZ2h.js.map → StatusDot-BniiEZ-W.js.map} +1 -1
- package/frontend/dist/assets/{ToggleGroup-DXyuwPy7.js → ToggleGroup-Df-pFUN3.js} +2 -2
- package/frontend/dist/assets/{ToggleGroup-DXyuwPy7.js.map → ToggleGroup-Df-pFUN3.js.map} +1 -1
- package/frontend/dist/assets/{UsersPage-BiUn8c0z.js → UsersPage-je7LsHJG.js} +2 -2
- package/frontend/dist/assets/{UsersPage-BiUn8c0z.js.map → UsersPage-je7LsHJG.js.map} +1 -1
- package/frontend/dist/assets/{clipboard-7MOQ-4Cg.js → clipboard-BrGjez3U.js} +2 -2
- package/frontend/dist/assets/{clipboard-7MOQ-4Cg.js.map → clipboard-BrGjez3U.js.map} +1 -1
- package/frontend/dist/assets/index-DL9uGzQF.js +3 -0
- package/frontend/dist/assets/{index-BKFFbupY.js.map → index-DL9uGzQF.js.map} +1 -1
- package/frontend/dist/assets/{index-B3hkLjCv.css → index-JDk4RjZI.css} +1 -1
- package/frontend/dist/assets/{resourceService-BOn21QbP.js → resourceService-8K1eUH8N.js} +2 -2
- package/frontend/dist/assets/{resourceService-BOn21QbP.js.map → resourceService-8K1eUH8N.js.map} +1 -1
- package/frontend/dist/assets/{useCostData-3iIVsNH6.js → useCostData-BuDleGRv.js} +2 -2
- package/frontend/dist/assets/{useCostData-3iIVsNH6.js.map → useCostData-BuDleGRv.js.map} +1 -1
- package/frontend/dist/assets/variableDetection-Djnw5--_.js +18 -0
- package/frontend/dist/assets/variableDetection-Djnw5--_.js.map +1 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/GroupsPage-DtoTWqWU.js +0 -33
- package/frontend/dist/assets/GroupsPage-DtoTWqWU.js.map +0 -1
- package/frontend/dist/assets/ServersPage-C7000Szj.js.map +0 -1
- package/frontend/dist/assets/index-BKFFbupY.js +0 -3
- package/frontend/dist/assets/variableDetection-Cpa0rIrf.js +0 -18
- package/frontend/dist/assets/variableDetection-Cpa0rIrf.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, client, 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,6 +2294,7 @@ export const getServersInfo = async (page, limit, user) => {
|
|
|
2154
2294
|
return {
|
|
2155
2295
|
name,
|
|
2156
2296
|
version,
|
|
2297
|
+
recovery: enabled ? recovery : undefined,
|
|
2157
2298
|
protocolVersion: enabled && status === 'connected' && resolvedType !== 'openapi'
|
|
2158
2299
|
? client?.getNegotiatedProtocolVersion?.()
|
|
2159
2300
|
: undefined,
|
|
@@ -2163,6 +2304,7 @@ export const getServersInfo = async (page, limit, user) => {
|
|
|
2163
2304
|
status,
|
|
2164
2305
|
error,
|
|
2165
2306
|
tools: toolsWithEnabled,
|
|
2307
|
+
...(capabilityCache ? { capabilityCache } : {}),
|
|
2166
2308
|
prompts: promptsWithEnabled,
|
|
2167
2309
|
resources: resourcesWithEnabled,
|
|
2168
2310
|
createTime,
|
|
@@ -2241,6 +2383,7 @@ export const reconnectServer = async (serverName) => {
|
|
|
2241
2383
|
}
|
|
2242
2384
|
// Close existing connection if any
|
|
2243
2385
|
if (serverInfo.client) {
|
|
2386
|
+
serverInfo.client.onclose = undefined;
|
|
2244
2387
|
try {
|
|
2245
2388
|
serverInfo.client.close();
|
|
2246
2389
|
}
|
|
@@ -2262,7 +2405,7 @@ export const reconnectServer = async (serverName) => {
|
|
|
2262
2405
|
}
|
|
2263
2406
|
// Reinitialize the server
|
|
2264
2407
|
await initializeClientsFromSettings(false, serverName);
|
|
2265
|
-
logger.log(`
|
|
2408
|
+
logger.log(`Scheduled connection initialization for server: ${serverName}`);
|
|
2266
2409
|
};
|
|
2267
2410
|
/**
|
|
2268
2411
|
* Resolve the installed package version for an npx/uvx stdio server and record
|
|
@@ -2522,6 +2665,9 @@ const clearIdleShutdown = (serverInfo) => {
|
|
|
2522
2665
|
};
|
|
2523
2666
|
const closeServerRuntime = (serverInfo) => {
|
|
2524
2667
|
clearIdleShutdown(serverInfo);
|
|
2668
|
+
if (serverInfo.client)
|
|
2669
|
+
serverInfo.client.onclose = undefined;
|
|
2670
|
+
serverInfo.recovery = undefined;
|
|
2525
2671
|
if (serverInfo.keepAliveIntervalId) {
|
|
2526
2672
|
clearInterval(serverInfo.keepAliveIntervalId);
|
|
2527
2673
|
serverInfo.keepAliveIntervalId = undefined;
|
|
@@ -2649,10 +2795,11 @@ const ensureServerReady = async (serverInfo) => {
|
|
|
2649
2795
|
if (expandedConf.type === 'openapi') {
|
|
2650
2796
|
return;
|
|
2651
2797
|
}
|
|
2798
|
+
await prepareCapabilitySnapshot(serverInfo, expandedConf);
|
|
2652
2799
|
const name = rawConfig.name;
|
|
2653
2800
|
logger.log(`[${name}] Cold-starting on-demand server…`);
|
|
2654
2801
|
const transport = await createTransportFromConfig(name, expandedConf);
|
|
2655
|
-
const client = createUpstreamMcpClient(name, () => serverInfo);
|
|
2802
|
+
const client = createUpstreamMcpClient(name, () => serverInfo, expandedConf);
|
|
2656
2803
|
const serverRequestOptions = expandedConf.options || {};
|
|
2657
2804
|
const defaultRequestTimeout = Number(process.env.DEFAULT_REQUEST_TIMEOUT) || 60000;
|
|
2658
2805
|
const requestOptions = {
|
|
@@ -2690,6 +2837,11 @@ const ensureServerReady = async (serverInfo) => {
|
|
|
2690
2837
|
});
|
|
2691
2838
|
}
|
|
2692
2839
|
}
|
|
2840
|
+
else {
|
|
2841
|
+
updateServerToolsCache(serverInfo, []);
|
|
2842
|
+
broadcastToolListChanged();
|
|
2843
|
+
}
|
|
2844
|
+
await flushCapabilitySnapshot(serverInfo);
|
|
2693
2845
|
if (capabilities?.prompts) {
|
|
2694
2846
|
try {
|
|
2695
2847
|
const prompts = await client.listPrompts({}, requestOptions);
|
|
@@ -2753,6 +2905,7 @@ const primeOnDemandServers = (gate) => {
|
|
|
2753
2905
|
!hasPersonalAuthentication(si.config) &&
|
|
2754
2906
|
isStdioServer(si.config) &&
|
|
2755
2907
|
si.tools.length === 0 &&
|
|
2908
|
+
si.capabilityCache?.state !== 'fresh' &&
|
|
2756
2909
|
si.enabled !== false);
|
|
2757
2910
|
if (targets.length === 0)
|
|
2758
2911
|
return Promise.resolve();
|
|
@@ -2766,7 +2919,10 @@ const primeOnDemandServers = (gate) => {
|
|
|
2766
2919
|
await run(() => ensureServerReady(si),
|
|
2767
2920
|
// A reload may replace this runtime or finish its discovery while it
|
|
2768
2921
|
// waits for a slot. Neither needs another child from the old queue.
|
|
2769
|
-
() => serverInfos.includes(si) &&
|
|
2922
|
+
() => serverInfos.includes(si) &&
|
|
2923
|
+
si.enabled !== false &&
|
|
2924
|
+
si.tools.length === 0 &&
|
|
2925
|
+
si.capabilityCache?.state !== 'fresh');
|
|
2770
2926
|
// Sleep the child unless a tool call is using it; keep the cached tool list.
|
|
2771
2927
|
shutdownOnDemandServer(si);
|
|
2772
2928
|
logger.log('On-demand server tool cache primed');
|
|
@@ -3049,7 +3205,7 @@ const assertToolAvailableForRoute = (tool, appsRouteContext) => {
|
|
|
3049
3205
|
throw new Error(`Tool '${tool.name}' is only available to MCP Apps`);
|
|
3050
3206
|
}
|
|
3051
3207
|
};
|
|
3052
|
-
const resolveToolInGroup = async (group, toolName) => {
|
|
3208
|
+
const resolveToolInGroup = async (group, toolName, appsRouteContext) => {
|
|
3053
3209
|
const lookupGroup = getGroupLookupName(group);
|
|
3054
3210
|
if (!lookupGroup) {
|
|
3055
3211
|
return undefined;
|
|
@@ -3061,22 +3217,30 @@ const resolveToolInGroup = async (group, toolName) => {
|
|
|
3061
3217
|
const allowRawName = !isSmartRoutingGroup(group) &&
|
|
3062
3218
|
serverConfigsByName.size <= 1 &&
|
|
3063
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
|
+
}
|
|
3064
3225
|
for (const serverInfo of filteredServerInfos) {
|
|
3065
3226
|
// An idle on-demand server is selected here and woken by the caller (#1029)
|
|
3066
3227
|
if (!canServeToolRequests(serverInfo)) {
|
|
3067
3228
|
continue;
|
|
3068
3229
|
}
|
|
3069
3230
|
const serverConfig = serverConfigsByName.get(serverInfo.name);
|
|
3070
|
-
const internalToolName =
|
|
3231
|
+
const internalToolName = serverConfig?.preserveToolNames
|
|
3232
|
+
? `${serverInfo.name}${getNameSeparator()}${toolName}`
|
|
3233
|
+
: resolveNameFromGroup(toolName, serverInfo.name, serverConfig);
|
|
3071
3234
|
const tool = findToolOnServer(serverInfo, internalToolName, allowRawName, serverConfigsByName.size === 0);
|
|
3072
|
-
if (!tool) {
|
|
3235
|
+
if (!tool || (hasOriginalToolNames && !appsRouteContext.enabled && isAppOnlyTool(tool))) {
|
|
3073
3236
|
continue;
|
|
3074
3237
|
}
|
|
3075
3238
|
const filteredTools = await filterToolsByGroup(lookupGroup, serverInfo.name, [tool], serverConfig);
|
|
3076
3239
|
if (filteredTools.length === 0) {
|
|
3077
3240
|
continue;
|
|
3078
3241
|
}
|
|
3079
|
-
|
|
3242
|
+
// Retain the resolved identity when callers revalidate discovery after waking.
|
|
3243
|
+
return { serverInfo, toolName: tool.name, tool };
|
|
3080
3244
|
}
|
|
3081
3245
|
return undefined;
|
|
3082
3246
|
};
|
|
@@ -3130,7 +3294,7 @@ const projectToolForDownstream = (serverName, tool, appsRouteContext, serverConf
|
|
|
3130
3294
|
// Group and aggregate routes keep qualified names, including single-member groups.
|
|
3131
3295
|
return {
|
|
3132
3296
|
...projectedTool,
|
|
3133
|
-
name:
|
|
3297
|
+
name: projectGroupToolName(projectedTool.name, serverName, serverConfig, getNameSeparator()),
|
|
3134
3298
|
};
|
|
3135
3299
|
};
|
|
3136
3300
|
const listGroupTools = async (group, filteredServerInfos, serverConfigsByName, appsRouteContext) => {
|
|
@@ -3138,7 +3302,10 @@ const listGroupTools = async (group, filteredServerInfos, serverConfigsByName, a
|
|
|
3138
3302
|
// so this list reflects the freshly cached tools instead of returning empty.
|
|
3139
3303
|
// No wake is triggered from list itself; the prime handles that. See #1029.
|
|
3140
3304
|
await Promise.allSettled(filteredServerInfos
|
|
3141
|
-
.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)
|
|
3142
3309
|
.map((si) => si.spawningPromise));
|
|
3143
3310
|
const useRawName = !!group && !isSmartRoutingGroup(group) && !(await findRouteGroup(group));
|
|
3144
3311
|
const allTools = [];
|
|
@@ -3165,6 +3332,9 @@ const listGroupTools = async (group, filteredServerInfos, serverConfigsByName, a
|
|
|
3165
3332
|
}));
|
|
3166
3333
|
}
|
|
3167
3334
|
}
|
|
3335
|
+
if ([...serverConfigsByName.values()].some((config) => config.preserveToolNames)) {
|
|
3336
|
+
assertValidGroupToolNames(allTools);
|
|
3337
|
+
}
|
|
3168
3338
|
return allTools;
|
|
3169
3339
|
};
|
|
3170
3340
|
// Names the call handler always routes to a meta-tool on a Smart Routing session,
|
|
@@ -3354,7 +3524,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3354
3524
|
targetServerInfo = getVisibleServerByName(extra.server);
|
|
3355
3525
|
}
|
|
3356
3526
|
else if (getGroupLookupName(group)) {
|
|
3357
|
-
const groupTool = await resolveToolInGroup(group, toolName);
|
|
3527
|
+
const groupTool = await resolveToolInGroup(group, toolName, appsRouteContext);
|
|
3358
3528
|
if (groupTool) {
|
|
3359
3529
|
targetServerInfo = groupTool.serverInfo;
|
|
3360
3530
|
targetToolName = groupTool.toolName;
|
|
@@ -3379,6 +3549,8 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3379
3549
|
if (targetServerInfo.config?.startOnDemand && targetServerInfo.status !== 'connected') {
|
|
3380
3550
|
await ensureServerReady(targetServerInfo);
|
|
3381
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;
|
|
3382
3554
|
const freshStatus = targetServerInfo.status;
|
|
3383
3555
|
if (freshStatus !== 'connected') {
|
|
3384
3556
|
throw new Error(`Failed to start on-demand server '${targetServerInfo.name}' — check server logs`);
|
|
@@ -3552,13 +3724,13 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3552
3724
|
// Apps support; keep legacy raw aggregate calls compatible.
|
|
3553
3725
|
const singleServerAppsRoute = !getGroupLookupName(group) && !!appsRouteContext.serverInfo;
|
|
3554
3726
|
const groupTool = !singleServerAppsRoute && lookupGroup
|
|
3555
|
-
? await resolveToolInGroup(group, request.params.name)
|
|
3727
|
+
? await resolveToolInGroup(group, request.params.name, appsRouteContext)
|
|
3556
3728
|
: undefined;
|
|
3557
3729
|
const serverInfo = singleServerAppsRoute
|
|
3558
3730
|
? appsRouteContext.serverInfo
|
|
3559
3731
|
: (groupTool?.serverInfo ?? (lookupGroup ? undefined : getServerByTool(request.params.name)));
|
|
3560
3732
|
const routeToolName = groupTool?.toolName ?? request.params.name;
|
|
3561
|
-
|
|
3733
|
+
let tool = groupTool?.tool ??
|
|
3562
3734
|
(serverInfo ? findToolOnServer(serverInfo, routeToolName, singleServerAppsRoute) : undefined);
|
|
3563
3735
|
if (!serverInfo || !tool) {
|
|
3564
3736
|
throw new ToolUnavailableError(`Tool not available: ${request.params.name}`, classifyUnavailableReason(request.params.name));
|
|
@@ -3571,6 +3743,10 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3571
3743
|
releaseOnDemandToolCall = beginOnDemandToolCall(serverInfo);
|
|
3572
3744
|
if (serverInfo.config?.startOnDemand && serverInfo.status !== 'connected') {
|
|
3573
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);
|
|
3574
3750
|
}
|
|
3575
3751
|
serverInfo.lastUsedAt = Date.now();
|
|
3576
3752
|
// Handle OpenAPI servers differently
|
|
@@ -4121,10 +4297,41 @@ const handleReadResourceRequestImpl = async (request, extra) => {
|
|
|
4121
4297
|
};
|
|
4122
4298
|
}
|
|
4123
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
|
+
});
|
|
4124
4331
|
// Personal runtimes never enter serverInfos or config exports. Their tool list
|
|
4125
4332
|
// is added to the rows under the server name, the index the reindex endpoint
|
|
4126
4333
|
// rebuilds from every binding.
|
|
4127
|
-
const createPrincipalRuntime = async (name, resolvedConfig) => {
|
|
4334
|
+
const createPrincipalRuntime = async (name, resolvedConfig, scope, toolsOnly = false) => {
|
|
4128
4335
|
const info = {
|
|
4129
4336
|
name,
|
|
4130
4337
|
owner: resolvedConfig.owner,
|
|
@@ -4140,41 +4347,22 @@ const createPrincipalRuntime = async (name, resolvedConfig) => {
|
|
|
4140
4347
|
resources: [],
|
|
4141
4348
|
createTime: Date.now(),
|
|
4142
4349
|
};
|
|
4143
|
-
const
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
const value = Reflect.get(target, property);
|
|
4155
|
-
if (typeof value !== 'function')
|
|
4156
|
-
return value;
|
|
4157
|
-
if (!safeMethods.has(String(property)))
|
|
4158
|
-
return value.bind(target);
|
|
4159
|
-
return async (...args) => {
|
|
4160
|
-
try {
|
|
4161
|
-
return await value.apply(target, args);
|
|
4162
|
-
}
|
|
4163
|
-
catch (error) {
|
|
4164
|
-
if (isUpstreamConnectionFailure(error))
|
|
4165
|
-
info.status = 'disconnected';
|
|
4166
|
-
const failure = createUpstreamRequestError(name, String(property), error, resolvedConfig);
|
|
4167
|
-
logger.error(failure.message);
|
|
4168
|
-
throw failure;
|
|
4169
|
-
}
|
|
4170
|
-
};
|
|
4171
|
-
},
|
|
4172
|
-
});
|
|
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
|
+
}
|
|
4173
4361
|
try {
|
|
4174
4362
|
if (resolvedConfig.type === 'openapi') {
|
|
4175
4363
|
const client = new OpenAPIClient(resolvedConfig);
|
|
4176
4364
|
await client.initialize();
|
|
4177
|
-
info.openApiClient =
|
|
4365
|
+
info.openApiClient = protectPersonalClient(client, info, resolvedConfig);
|
|
4178
4366
|
info.tools = client.getTools().map((tool) => ({
|
|
4179
4367
|
...tool,
|
|
4180
4368
|
name: `${name}${getNameSeparator()}${tool.name}`,
|
|
@@ -4184,17 +4372,23 @@ const createPrincipalRuntime = async (name, resolvedConfig) => {
|
|
|
4184
4372
|
else {
|
|
4185
4373
|
const transport = await createTransportFromConfig(name, resolvedConfig);
|
|
4186
4374
|
const client = new LegacyMcpClient({ name: `mcp-client-${name}`, version: '1.0.0' }, {
|
|
4375
|
+
versionNegotiation: { mode: resolveUpstreamProtocolMode(resolvedConfig) },
|
|
4187
4376
|
capabilities: MCP_APPS_CAPABILITIES,
|
|
4188
4377
|
jsonSchemaValidator: new ResilientJsonSchemaValidator(),
|
|
4189
4378
|
});
|
|
4190
4379
|
info.transport = transport;
|
|
4191
|
-
info.client =
|
|
4380
|
+
info.client = protectPersonalClient(client, info, resolvedConfig);
|
|
4192
4381
|
await client.connect(transport, resolvedConfig.options || {});
|
|
4193
4382
|
const capabilities = client.getServerCapabilities();
|
|
4194
4383
|
if (capabilities?.tools) {
|
|
4195
4384
|
const { tools } = await client.listTools({}, resolvedConfig.options);
|
|
4196
4385
|
info.tools = preserveListFreshness(tools, tools.map((tool) => normalizeToolForCache(name, tool)));
|
|
4386
|
+
recordCapabilitySnapshot(info, tools);
|
|
4197
4387
|
}
|
|
4388
|
+
else {
|
|
4389
|
+
recordCapabilitySnapshot(info, []);
|
|
4390
|
+
}
|
|
4391
|
+
await flushCapabilitySnapshot(info);
|
|
4198
4392
|
if (capabilities?.prompts) {
|
|
4199
4393
|
const { prompts } = await client.listPrompts({}, resolvedConfig.options);
|
|
4200
4394
|
info.prompts = preserveListFreshness(prompts, prompts.map((prompt) => normalizePromptForCache(name, prompt)));
|
|
@@ -4229,7 +4423,7 @@ const createPrincipalRuntime = async (name, resolvedConfig) => {
|
|
|
4229
4423
|
throw failure;
|
|
4230
4424
|
}
|
|
4231
4425
|
};
|
|
4232
|
-
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));
|
|
4233
4427
|
/**
|
|
4234
4428
|
* Resolve the tools of a personal-credential server on behalf of an explicit
|
|
4235
4429
|
* principal, outside any request context.
|
|
@@ -4246,12 +4440,112 @@ const principalRuntimes = new PrincipalRuntimeService(createPrincipalRuntime, cl
|
|
|
4246
4440
|
export const getServerToolsForPrincipal = async (serverName, principal) => {
|
|
4247
4441
|
const lease = await principalRuntimes.acquire(serverName, principal);
|
|
4248
4442
|
try {
|
|
4443
|
+
await refreshModernServerLists(lease.info, ['tools']);
|
|
4249
4444
|
return lease.info.tools ?? [];
|
|
4250
4445
|
}
|
|
4251
4446
|
finally {
|
|
4252
4447
|
lease.release();
|
|
4253
4448
|
}
|
|
4254
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
|
+
};
|
|
4255
4549
|
const withPrincipalServers = (handler, operation) => async (request, extra) => {
|
|
4256
4550
|
if (!serverInfos.some((info) => hasPersonalAuthentication(info.config)))
|
|
4257
4551
|
return handler(request, extra);
|
|
@@ -4277,7 +4571,10 @@ const withPrincipalServers = (handler, operation) => async (request, extra) => {
|
|
|
4277
4571
|
if (extra?.server && !target && (operation === 'tool' || operation === 'prompt')) {
|
|
4278
4572
|
throw new ToolUnavailableError(`Tool not available: ${requestedName}`, 'server-or-name-not-found');
|
|
4279
4573
|
}
|
|
4280
|
-
if (!extra?.server &&
|
|
4574
|
+
if (!extra?.server &&
|
|
4575
|
+
typeof requestedName === 'string' &&
|
|
4576
|
+
!(operation === 'tool' &&
|
|
4577
|
+
[...serverConfigsByName.values()].some((config) => config.preserveToolNames))) {
|
|
4281
4578
|
let longestPrefix = 0;
|
|
4282
4579
|
for (const info of candidates) {
|
|
4283
4580
|
const exposedName = getExposedServerName(info.name, serverConfigsByName.get(info.name));
|
|
@@ -4306,7 +4603,7 @@ const withPrincipalServers = (handler, operation) => async (request, extra) => {
|
|
|
4306
4603
|
throw hasPersonalOAuth(info.config)
|
|
4307
4604
|
? personalAuthorizationRequired(info.name)
|
|
4308
4605
|
: missingCredentialError(info.name);
|
|
4309
|
-
const lease = await principalRuntimes.acquire(info.name, user);
|
|
4606
|
+
const lease = await principalRuntimes.acquire(info.name, user, operation === 'tools-list');
|
|
4310
4607
|
releases.push(lease.release);
|
|
4311
4608
|
scoped.set(info.name, lease.info);
|
|
4312
4609
|
}
|
|
@@ -4317,7 +4614,10 @@ const withPrincipalServers = (handler, operation) => async (request, extra) => {
|
|
|
4317
4614
|
}
|
|
4318
4615
|
}));
|
|
4319
4616
|
const requiredTarget = target ||
|
|
4320
|
-
(candidates.length === 1 &&
|
|
4617
|
+
(candidates.length === 1 &&
|
|
4618
|
+
operation !== 'list' &&
|
|
4619
|
+
operation !== 'tools-list' &&
|
|
4620
|
+
requestedName !== 'search_tools'
|
|
4321
4621
|
? templated[0]
|
|
4322
4622
|
: undefined);
|
|
4323
4623
|
const failure = requiredTarget ? failures.get(requiredTarget.name) : undefined;
|
|
@@ -4446,14 +4746,16 @@ const withCacheHints = (handler, passthroughTtl = false, listField) => async (re
|
|
|
4446
4746
|
// and capabilities. Shared caches must not reuse the gateway result across users.
|
|
4447
4747
|
return { ...result, ttlMs, cacheScope: 'private' };
|
|
4448
4748
|
};
|
|
4449
|
-
export const handleListToolsRequest = withPrincipalServers(withCacheHints(handleListToolsRequestImpl, false, 'tools'), 'list');
|
|
4450
|
-
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');
|
|
4451
4751
|
export const handleCallToolRequest = (request, extra) => observeToolCall(() => callToolWithPrincipal(request, extra));
|
|
4452
|
-
export const handleGetPromptRequest = withPrincipalServers(handleGetPromptRequestImpl, 'prompt');
|
|
4453
|
-
export const handleListPromptsRequest = withPrincipalServers(withCacheHints(handleListPromptsRequestImpl, false, 'prompts'), 'list');
|
|
4454
|
-
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');
|
|
4455
4757
|
export const handleListResourceTemplatesRequest = withPrincipalServers(withCacheHints(handleListResourceTemplatesRequestImpl, false, 'resourceTemplates'), 'list');
|
|
4456
|
-
export const handleReadResourceRequest = withPrincipalServers(withCacheHints(handleReadResourceRequestImpl, true), 'resource');
|
|
4758
|
+
export const handleReadResourceRequest = withPrincipalServers(withModernLists(withCacheHints(handleReadResourceRequestImpl, true), ['resources']), 'resource');
|
|
4457
4759
|
export const createMcpServer = (name, version, options) => {
|
|
4458
4760
|
const normalizedOptions = typeof options === 'string' ? { group: options } : (options ?? {});
|
|
4459
4761
|
// Determine server name based on routing type
|