@samanhappy/mcphub 1.0.35 → 1.0.37
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.fr.md +48 -17
- package/README.md +50 -39
- package/README.zh.md +50 -37
- package/dist/clients/openapi.js +32 -1
- package/dist/clients/openapi.js.map +1 -1
- package/dist/controllers/credentialBindingController.js +56 -0
- package/dist/controllers/credentialBindingController.js.map +1 -0
- package/dist/controllers/groupController.js +63 -14
- package/dist/controllers/groupController.js.map +1 -1
- package/dist/controllers/serverController.js +26 -4
- package/dist/controllers/serverController.js.map +1 -1
- package/dist/controllers/userController.js +3 -0
- package/dist/controllers/userController.js.map +1 -1
- package/dist/dao/CredentialBindingDao.js +48 -0
- package/dist/dao/CredentialBindingDao.js.map +1 -0
- package/dist/dao/CredentialBindingDaoDbImpl.js +20 -0
- package/dist/dao/CredentialBindingDaoDbImpl.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/GroupDaoDbImpl.js +18 -0
- package/dist/dao/GroupDaoDbImpl.js.map +1 -1
- package/dist/dao/ServerDaoDbImpl.js +3 -0
- package/dist/dao/ServerDaoDbImpl.js.map +1 -1
- package/dist/db/connection.js +3 -2
- package/dist/db/connection.js.map +1 -1
- package/dist/db/entities/CredentialBinding.js +33 -0
- package/dist/db/entities/CredentialBinding.js.map +1 -0
- package/dist/db/entities/Group.js +9 -1
- 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/groupSchema.js +29 -0
- package/dist/db/groupSchema.js.map +1 -0
- package/dist/middlewares/auth.js +2 -0
- package/dist/middlewares/auth.js.map +1 -1
- package/dist/middlewares/i18n.js +2 -1
- package/dist/middlewares/i18n.js.map +1 -1
- package/dist/routes/index.js +19 -4
- package/dist/routes/index.js.map +1 -1
- package/dist/services/activityLoggingService.js +21 -8
- package/dist/services/activityLoggingService.js.map +1 -1
- package/dist/services/credentialBindingService.js +193 -0
- package/dist/services/credentialBindingService.js.map +1 -0
- package/dist/services/groupService.js +57 -23
- package/dist/services/groupService.js.map +1 -1
- package/dist/services/logService.js +11 -2
- package/dist/services/logService.js.map +1 -1
- package/dist/services/mcpService.js +478 -77
- package/dist/services/mcpService.js.map +1 -1
- package/dist/services/oauthClientRegistration.js +22 -7
- package/dist/services/oauthClientRegistration.js.map +1 -1
- package/dist/services/oauthSettingsStore.js +3 -0
- package/dist/services/oauthSettingsStore.js.map +1 -1
- package/dist/services/principalRuntimeService.js +109 -0
- package/dist/services/principalRuntimeService.js.map +1 -0
- package/dist/services/serverConfigPresenter.js +3 -0
- package/dist/services/serverConfigPresenter.js.map +1 -1
- package/dist/services/sseService.js +65 -16
- package/dist/services/sseService.js.map +1 -1
- package/dist/utils/credentialTemplate.js +38 -0
- package/dist/utils/credentialTemplate.js.map +1 -0
- package/dist/utils/groupAccess.js +17 -0
- package/dist/utils/groupAccess.js.map +1 -0
- package/dist/utils/migration.js +11 -0
- package/dist/utils/migration.js.map +1 -1
- package/dist/utils/rateLimit.js +85 -18
- package/dist/utils/rateLimit.js.map +1 -1
- package/dist/utils/serverConfigPersistence.js +2 -0
- package/dist/utils/serverConfigPersistence.js.map +1 -1
- package/frontend/dist/assets/{ActivityPage-B4OBJUBq.js → ActivityPage-Bx5-Jrxq.js} +2 -2
- package/frontend/dist/assets/{ActivityPage-B4OBJUBq.js.map → ActivityPage-Bx5-Jrxq.js.map} +1 -1
- package/frontend/dist/assets/{ConfirmDialog-3APts2Y9.js → ConfirmDialog-uWBwm8J1.js} +2 -2
- package/frontend/dist/assets/{ConfirmDialog-3APts2Y9.js.map → ConfirmDialog-uWBwm8J1.js.map} +1 -1
- package/frontend/dist/assets/CredentialsPage-BGbk_kBx.js +2 -0
- package/frontend/dist/assets/CredentialsPage-BGbk_kBx.js.map +1 -0
- package/frontend/dist/assets/{Dashboard-czP-DGHn.js → Dashboard-gHeXLPEn.js} +2 -2
- package/frontend/dist/assets/{Dashboard-czP-DGHn.js.map → Dashboard-gHeXLPEn.js.map} +1 -1
- package/frontend/dist/assets/{DeleteDialog-D9ycSRvn.js → DeleteDialog-BYhrMGZo.js} +2 -2
- package/frontend/dist/assets/{DeleteDialog-D9ycSRvn.js.map → DeleteDialog-BYhrMGZo.js.map} +1 -1
- package/frontend/dist/assets/{EndpointCopy-CDwtdCHE.js → EndpointCopy-BRwX7Dpv.js} +2 -2
- package/frontend/dist/assets/{EndpointCopy-CDwtdCHE.js.map → EndpointCopy-BRwX7Dpv.js.map} +1 -1
- package/frontend/dist/assets/GroupsPage-grCAx55k.js +33 -0
- package/frontend/dist/assets/GroupsPage-grCAx55k.js.map +1 -0
- package/frontend/dist/assets/{LoginPage-BBzViqLx.js → LoginPage-C7-m2jmB.js} +2 -2
- package/frontend/dist/assets/{LoginPage-BBzViqLx.js.map → LoginPage-C7-m2jmB.js.map} +1 -1
- package/frontend/dist/assets/{LogsPage-CqbaqFiQ.js → LogsPage-B00XTokA.js} +2 -2
- package/frontend/dist/assets/{LogsPage-CqbaqFiQ.js.map → LogsPage-B00XTokA.js.map} +1 -1
- package/frontend/dist/assets/{MarketPage-DinCKwrj.js → MarketPage-CVJMdSDD.js} +2 -2
- package/frontend/dist/assets/{MarketPage-DinCKwrj.js.map → MarketPage-CVJMdSDD.js.map} +1 -1
- package/frontend/dist/assets/{OAuthConsentPage-BuPcJ2zS.js → OAuthConsentPage-znW3HtpR.js} +2 -2
- package/frontend/dist/assets/{OAuthConsentPage-BuPcJ2zS.js.map → OAuthConsentPage-znW3HtpR.js.map} +1 -1
- package/frontend/dist/assets/{Pagination-Dwko3g48.js → Pagination-CLUvn47x.js} +2 -2
- package/frontend/dist/assets/{Pagination-Dwko3g48.js.map → Pagination-CLUvn47x.js.map} +1 -1
- package/frontend/dist/assets/{PromptsPage-CJ-wYRAL.js → PromptsPage-DGcNS3fO.js} +2 -2
- package/frontend/dist/assets/{PromptsPage-CJ-wYRAL.js.map → PromptsPage-DGcNS3fO.js.map} +1 -1
- package/frontend/dist/assets/{ResourcesPage-DRV8gnZV.js → ResourcesPage-CfyZoadu.js} +2 -2
- package/frontend/dist/assets/{ResourcesPage-DRV8gnZV.js.map → ResourcesPage-CfyZoadu.js.map} +1 -1
- package/frontend/dist/assets/ServersPage-BlC0RdIu.js +39 -0
- package/frontend/dist/assets/ServersPage-BlC0RdIu.js.map +1 -0
- package/frontend/dist/assets/{SettingsPage-Czv4OhtK.js → SettingsPage-CiVNDyr_.js} +2 -2
- package/frontend/dist/assets/{SettingsPage-Czv4OhtK.js.map → SettingsPage-CiVNDyr_.js.map} +1 -1
- package/frontend/dist/assets/{StatusDot-CwpmIlJi.js → StatusDot-DYO56shE.js} +2 -2
- package/frontend/dist/assets/{StatusDot-CwpmIlJi.js.map → StatusDot-DYO56shE.js.map} +1 -1
- package/frontend/dist/assets/{ToggleGroup-1qFTkDqh.js → ToggleGroup-r8yGt_Fq.js} +2 -2
- package/frontend/dist/assets/{ToggleGroup-1qFTkDqh.js.map → ToggleGroup-r8yGt_Fq.js.map} +1 -1
- package/frontend/dist/assets/{UsersPage-CAMXgywh.js → UsersPage-B3muXE90.js} +2 -2
- package/frontend/dist/assets/{UsersPage-CAMXgywh.js.map → UsersPage-B3muXE90.js.map} +1 -1
- package/frontend/dist/assets/{framework-vendor-BHAQX05P.js → framework-vendor-_YL27Pfc.js} +2 -2
- package/frontend/dist/assets/{framework-vendor-BHAQX05P.js.map → framework-vendor-_YL27Pfc.js.map} +1 -1
- package/frontend/dist/assets/{i18n-vendor-CMcB7XUR.js → i18n-vendor-BMaj5qV8.js} +2 -2
- package/frontend/dist/assets/{i18n-vendor-CMcB7XUR.js.map → i18n-vendor-BMaj5qV8.js.map} +1 -1
- package/frontend/dist/assets/{icons-vendor-HZHHWeTa.js → icons-vendor-VW-UwEhw.js} +3 -3
- package/frontend/dist/assets/{icons-vendor-HZHHWeTa.js.map → icons-vendor-VW-UwEhw.js.map} +1 -1
- package/frontend/dist/assets/index-B-aT0HW5.js +3 -0
- package/frontend/dist/assets/index-B-aT0HW5.js.map +1 -0
- package/frontend/dist/assets/{index-DtrCVU1h.css → index-B3OqAhGz.css} +1 -1
- package/frontend/dist/assets/{resourceService-BTVgor9F.js → resourceService-CSXOSi7M.js} +2 -2
- package/frontend/dist/assets/{resourceService-BTVgor9F.js.map → resourceService-CSXOSi7M.js.map} +1 -1
- package/frontend/dist/assets/{useCostData-CDVGUx_e.js → useCostData-BOxUFQAH.js} +2 -2
- package/frontend/dist/assets/{useCostData-CDVGUx_e.js.map → useCostData-BOxUFQAH.js.map} +1 -1
- package/frontend/dist/assets/useSettingsData-DmpLlRI6.js +2 -0
- package/frontend/dist/assets/{useSettingsData-92nglN_X.js.map → useSettingsData-DmpLlRI6.js.map} +1 -1
- package/frontend/dist/assets/variableDetection-cmwwuSWB.js +16 -0
- package/frontend/dist/assets/variableDetection-cmwwuSWB.js.map +1 -0
- package/frontend/dist/index.html +5 -5
- package/package.json +4 -5
- package/frontend/dist/assets/GroupsPage-BgUHjkGb.js +0 -33
- package/frontend/dist/assets/GroupsPage-BgUHjkGb.js.map +0 -1
- package/frontend/dist/assets/ServersPage-CbtcAcVH.js +0 -39
- package/frontend/dist/assets/ServersPage-CbtcAcVH.js.map +0 -1
- package/frontend/dist/assets/index-Cb6IBpZR.js +0 -3
- package/frontend/dist/assets/index-Cb6IBpZR.js.map +0 -1
- package/frontend/dist/assets/useSettingsData-92nglN_X.js +0 -2
- package/frontend/dist/assets/variableDetection-BlOwsGsz.js +0 -16
- package/frontend/dist/assets/variableDetection-BlOwsGsz.js.map +0 -1
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { canAccessGroupRoute } from '../utils/groupAccess.js';
|
|
3
|
+
import { PrincipalRuntimeService } from './principalRuntimeService.js';
|
|
4
|
+
import { UserContextService } from './userContextService.js';
|
|
5
|
+
import { deleteCredentialBindings, missingCredentialError } from './credentialBindingService.js';
|
|
6
|
+
import { CredentialBindingError, hasCredentialTemplate, validateCredentialTemplate } from '../utils/credentialTemplate.js';
|
|
1
7
|
import os from 'os';
|
|
2
8
|
import path from 'path';
|
|
3
9
|
import fs from 'fs';
|
|
@@ -519,8 +525,14 @@ const normalizeResourceForCache = (resource) => {
|
|
|
519
525
|
};
|
|
520
526
|
// Store all server information
|
|
521
527
|
let serverInfos = [];
|
|
528
|
+
// Bumped by every initializeClientsFromSettings pass. Connections are started
|
|
529
|
+
// fire-and-forget and settle long after the pass that started them returned, so
|
|
530
|
+
// a callback needs to know whether a later pass has run before it writes its
|
|
531
|
+
// result anywhere. See the `liveServerInfo` helper in that function.
|
|
532
|
+
let initGeneration = 0;
|
|
533
|
+
const principalServerScope = new AsyncLocalStorage();
|
|
522
534
|
const getVisibleServerInfos = () => {
|
|
523
|
-
return getDataService().filterData(serverInfos);
|
|
535
|
+
return getDataService().filterData(principalServerScope.getStore() ?? serverInfos);
|
|
524
536
|
};
|
|
525
537
|
const getVisibleServerByName = (name) => {
|
|
526
538
|
return getVisibleServerInfos().find((serverInfo) => serverInfo.name === name);
|
|
@@ -656,7 +668,8 @@ export const summarizeServerConnections = (infos) => {
|
|
|
656
668
|
};
|
|
657
669
|
};
|
|
658
670
|
export const getServerConnectionStats = () => {
|
|
659
|
-
|
|
671
|
+
// Personal definitions have no shared connection whose readiness can be checked.
|
|
672
|
+
return summarizeServerConnections(serverInfos.filter((info) => !hasCredentialTemplate(info.config)));
|
|
660
673
|
};
|
|
661
674
|
// Returns true if all enabled servers are connected
|
|
662
675
|
export const connected = () => {
|
|
@@ -665,6 +678,7 @@ export const connected = () => {
|
|
|
665
678
|
};
|
|
666
679
|
// Global cleanup function to close all connections
|
|
667
680
|
export const cleanupAllServers = () => {
|
|
681
|
+
principalRuntimes.invalidate();
|
|
668
682
|
for (const serverInfo of serverInfos) {
|
|
669
683
|
try {
|
|
670
684
|
if (serverInfo.client) {
|
|
@@ -927,8 +941,10 @@ export const createTransportFromConfig = async (name, conf) => {
|
|
|
927
941
|
let transport;
|
|
928
942
|
const env = {
|
|
929
943
|
...process.env,
|
|
930
|
-
...replaceEnvVars(conf.env || {}),
|
|
944
|
+
...(hasCredentialTemplate(conf) ? conf.env : replaceEnvVars(conf.env || {})),
|
|
931
945
|
};
|
|
946
|
+
// The encryption key belongs only to the hub, never to a child process.
|
|
947
|
+
delete env.MCPHUB_CREDENTIAL_ENCRYPTION_KEY;
|
|
932
948
|
// SSRF guard: block URL/streamable-http transports from reaching
|
|
933
949
|
// loopback / RFC1918 / link-local targets (e.g. cloud metadata service).
|
|
934
950
|
// Admin-owned servers may legitimately target internal services, so they
|
|
@@ -941,11 +957,11 @@ export const createTransportFromConfig = async (name, conf) => {
|
|
|
941
957
|
}
|
|
942
958
|
if (conf.type === 'streamable-http') {
|
|
943
959
|
const options = {};
|
|
944
|
-
let headers = conf.headers ? replaceEnvVars(conf.headers, env) : {};
|
|
960
|
+
let headers = conf.headers ? (hasCredentialTemplate(conf) ? conf.headers : replaceEnvVars(conf.headers, env)) : {};
|
|
945
961
|
const baseFetch = createAbortIsolatingFetch(createFetchWithProxy(getProxyConfigFromEnv(env)));
|
|
946
962
|
const requestAwareFetch = createRedirectValidatingFetch(createRequestContextAwareFetch(baseFetch, conf.passthroughHeaders), allowInternal);
|
|
947
963
|
// Create OAuth provider if configured - SDK will handle authentication automatically
|
|
948
|
-
const authProvider = await createOAuthProvider(name, conf);
|
|
964
|
+
const authProvider = hasCredentialTemplate(conf) ? undefined : await createOAuthProvider(name, conf);
|
|
949
965
|
if (authProvider) {
|
|
950
966
|
options.authProvider = authProvider;
|
|
951
967
|
// When the OAuth provider is active, strip any static Authorization
|
|
@@ -968,11 +984,11 @@ export const createTransportFromConfig = async (name, conf) => {
|
|
|
968
984
|
else if (conf.url) {
|
|
969
985
|
// SSE transport
|
|
970
986
|
const options = {};
|
|
971
|
-
let headers = conf.headers ? replaceEnvVars(conf.headers, env) : {};
|
|
987
|
+
let headers = conf.headers ? (hasCredentialTemplate(conf) ? conf.headers : replaceEnvVars(conf.headers, env)) : {};
|
|
972
988
|
const baseFetch = createAbortIsolatingFetch(createFetchWithProxy(getProxyConfigFromEnv(env)));
|
|
973
989
|
const requestAwareFetch = createRedirectValidatingFetch(createRequestContextAwareFetch(baseFetch, conf.passthroughHeaders), allowInternal);
|
|
974
990
|
// Create OAuth provider if configured - SDK will handle authentication automatically
|
|
975
|
-
const authProvider = await createOAuthProvider(name, conf);
|
|
991
|
+
const authProvider = hasCredentialTemplate(conf) ? undefined : await createOAuthProvider(name, conf);
|
|
976
992
|
if (authProvider) {
|
|
977
993
|
options.authProvider = authProvider;
|
|
978
994
|
// Drop static Authorization header when OAuth manages auth (see above).
|
|
@@ -1032,7 +1048,10 @@ export const createTransportFromConfig = async (name, conf) => {
|
|
|
1032
1048
|
env: env,
|
|
1033
1049
|
stderr: 'pipe',
|
|
1034
1050
|
});
|
|
1035
|
-
if (transport.stderr) {
|
|
1051
|
+
if (transport.stderr && hasCredentialTemplate(conf)) {
|
|
1052
|
+
transport.stderr.on('data', () => undefined); // Personal upstream diagnostics can contain raw credentials.
|
|
1053
|
+
}
|
|
1054
|
+
else if (transport.stderr) {
|
|
1036
1055
|
observeStdioStderr(transport, transport.stderr, (message) => {
|
|
1037
1056
|
logger.log('Upstream server stderr', JSON.stringify({
|
|
1038
1057
|
serverName: name,
|
|
@@ -1056,6 +1075,9 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
|
|
|
1056
1075
|
if (!client) {
|
|
1057
1076
|
throw new Error(`Client not found for server: ${serverInfo.name}`);
|
|
1058
1077
|
}
|
|
1078
|
+
if (hasCredentialTemplate(serverInfo.config)) {
|
|
1079
|
+
return client.callTool(toolParams, undefined, options || {});
|
|
1080
|
+
}
|
|
1059
1081
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1060
1082
|
try {
|
|
1061
1083
|
const result = await client.callTool(toolParams, undefined, options || {});
|
|
@@ -1183,6 +1205,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1183
1205
|
const allServers = await getServerDao().findAll();
|
|
1184
1206
|
const existingServerInfos = serverInfos;
|
|
1185
1207
|
const nextServerInfos = [];
|
|
1208
|
+
const generation = ++initGeneration;
|
|
1186
1209
|
try {
|
|
1187
1210
|
for (const conf of allServers) {
|
|
1188
1211
|
const { name } = conf;
|
|
@@ -1215,6 +1238,16 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1215
1238
|
});
|
|
1216
1239
|
continue;
|
|
1217
1240
|
}
|
|
1241
|
+
if (hasCredentialTemplate(expandedConf)) {
|
|
1242
|
+
validateCredentialTemplate(expandedConf);
|
|
1243
|
+
nextServerInfos.push({
|
|
1244
|
+
name, owner: expandedConf.owner, visibility: expandedConf.visibility,
|
|
1245
|
+
sharedWithUsers: expandedConf.sharedWithUsers, enabled: true,
|
|
1246
|
+
status: 'disconnected', error: null, tools: [], prompts: [], resources: [],
|
|
1247
|
+
createTime: Date.now(), config: expandedConf,
|
|
1248
|
+
});
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1218
1251
|
// On-demand stdio servers: skip startup connect; spawn lazily on first
|
|
1219
1252
|
// tool call. startOnDemand is a stdio-only optimisation - HTTP/SSE and
|
|
1220
1253
|
// OpenAPI servers have no deferred child process, so they connect normally.
|
|
@@ -1222,23 +1255,25 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1222
1255
|
// connection) so the tool list stays visible to agents even while sleeping.
|
|
1223
1256
|
if (expandedConf.startOnDemand === true && isStdioServer(expandedConf)) {
|
|
1224
1257
|
const existingOnDemand = existingServerInfos.find((s) => s.name === name);
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1258
|
+
// Active calls, spawn promises and idle callbacks must share the same
|
|
1259
|
+
// runtime object across reloads, not copies of its mutable state.
|
|
1260
|
+
const onDemandInfo = existingOnDemand ?? {
|
|
1261
|
+
name,
|
|
1262
|
+
status: 'disconnected',
|
|
1263
|
+
error: null,
|
|
1264
|
+
tools: [],
|
|
1265
|
+
prompts: [],
|
|
1266
|
+
resources: [],
|
|
1267
|
+
createTime: Date.now(),
|
|
1268
|
+
};
|
|
1269
|
+
Object.assign(onDemandInfo, {
|
|
1236
1270
|
owner: expandedConf.owner,
|
|
1237
1271
|
visibility: expandedConf.visibility,
|
|
1238
1272
|
sharedWithUsers: expandedConf.sharedWithUsers,
|
|
1239
1273
|
enabled: true,
|
|
1240
1274
|
config: expandedConf,
|
|
1241
1275
|
});
|
|
1276
|
+
nextServerInfos.push(onDemandInfo);
|
|
1242
1277
|
logger.log(`Skipping startup connect for on-demand server: ${name}`);
|
|
1243
1278
|
continue;
|
|
1244
1279
|
}
|
|
@@ -1371,7 +1406,36 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1371
1406
|
}
|
|
1372
1407
|
}
|
|
1373
1408
|
else {
|
|
1374
|
-
|
|
1409
|
+
try {
|
|
1410
|
+
transport = await createTransportFromConfig(name, expandedConf);
|
|
1411
|
+
}
|
|
1412
|
+
catch (error) {
|
|
1413
|
+
// Fail this server only. Letting the error escape the loop hits the
|
|
1414
|
+
// outer catch, which restores `serverInfos` to its previous value -
|
|
1415
|
+
// empty at startup - so a single malformed config (an unparseable URL
|
|
1416
|
+
// rejected by assertSafeUrl, say) would drop every server from
|
|
1417
|
+
// getServersInfo and leave the whole fleet reporting 'connecting'.
|
|
1418
|
+
// The OpenAPI branch above already isolates its failures this way.
|
|
1419
|
+
logger.error('Failed to create transport for server', {
|
|
1420
|
+
serverName: name,
|
|
1421
|
+
error: summarizeErrorForLogging(error),
|
|
1422
|
+
});
|
|
1423
|
+
nextServerInfos.push({
|
|
1424
|
+
name,
|
|
1425
|
+
owner: expandedConf.owner,
|
|
1426
|
+
visibility: expandedConf.visibility,
|
|
1427
|
+
sharedWithUsers: expandedConf.sharedWithUsers,
|
|
1428
|
+
status: 'disconnected',
|
|
1429
|
+
error: `Failed to create transport: ${formatErrorForLogging(error)}`,
|
|
1430
|
+
tools: [],
|
|
1431
|
+
prompts: [],
|
|
1432
|
+
resources: [],
|
|
1433
|
+
createTime: Date.now(),
|
|
1434
|
+
enabled: true,
|
|
1435
|
+
config: expandedConf,
|
|
1436
|
+
});
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1375
1439
|
}
|
|
1376
1440
|
const serverInfoRef = {};
|
|
1377
1441
|
const client = createUpstreamMcpClient(name, () => serverInfoRef.current);
|
|
@@ -1417,12 +1481,39 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1417
1481
|
};
|
|
1418
1482
|
}
|
|
1419
1483
|
nextServerInfos.push(serverInfo);
|
|
1484
|
+
// The connect below settles long after this loop returns. If another
|
|
1485
|
+
// initialization pass has run by then, `serverInfo` may no longer be the
|
|
1486
|
+
// object `serverInfos` holds for this server - even when the reuse guard
|
|
1487
|
+
// "preserved" it, because preservation spreads the entry into a fresh
|
|
1488
|
+
// object. Writing the result into a replaced object leaves the server
|
|
1489
|
+
// stuck on 'connecting' for good while the log reports a successful
|
|
1490
|
+
// connect and a full tool list.
|
|
1491
|
+
//
|
|
1492
|
+
// While this pass is still the newest one, `serverInfo` is the entry that
|
|
1493
|
+
// will be published, so it is the right target even before the swap. Once
|
|
1494
|
+
// a later pass has run, resolve the live entry by name and only apply the
|
|
1495
|
+
// result if it still owns this client - which is exactly the case where
|
|
1496
|
+
// the later pass preserved this server rather than reconnecting it.
|
|
1497
|
+
const liveServerInfo = () => {
|
|
1498
|
+
if (generation === initGeneration)
|
|
1499
|
+
return serverInfo;
|
|
1500
|
+
const current = serverInfos.find((si) => si.name === name);
|
|
1501
|
+
return current && current.client === client ? current : undefined;
|
|
1502
|
+
};
|
|
1420
1503
|
connectClientWithDiagnostics(client, transport, initRequestOptions || requestOptions)
|
|
1421
1504
|
.then(() => {
|
|
1505
|
+
const info = liveServerInfo();
|
|
1506
|
+
if (!info) {
|
|
1507
|
+
// Nothing will ever read this connection again, so shut it down
|
|
1508
|
+
// rather than leaving its stdio child process running.
|
|
1509
|
+
logger.log(`Discarding superseded connection result for server: ${name}`);
|
|
1510
|
+
closeServerRuntime(serverInfo);
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1422
1513
|
logger.log(`Successfully connected client for server: ${name}`);
|
|
1423
1514
|
const serverVersion = client.getServerVersion?.();
|
|
1424
|
-
|
|
1425
|
-
|
|
1515
|
+
info.version = serverVersion?.version;
|
|
1516
|
+
info.instructions = client.getInstructions?.();
|
|
1426
1517
|
const capabilities = client.getServerCapabilities();
|
|
1427
1518
|
logger.log('Server capabilities', JSON.stringify(capabilities));
|
|
1428
1519
|
let dataError = null;
|
|
@@ -1430,8 +1521,11 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1430
1521
|
client
|
|
1431
1522
|
.listTools({}, initRequestOptions || requestOptions)
|
|
1432
1523
|
.then((tools) => {
|
|
1524
|
+
const target = liveServerInfo();
|
|
1525
|
+
if (!target)
|
|
1526
|
+
return;
|
|
1433
1527
|
logger.log(`Successfully listed ${tools.tools.length} tools for server: ${name}`);
|
|
1434
|
-
updateServerToolsCache(
|
|
1528
|
+
updateServerToolsCache(target, tools.tools, {
|
|
1435
1529
|
reportEmbeddingProgress: options?.reportEmbeddingProgress === true && serverName === name,
|
|
1436
1530
|
});
|
|
1437
1531
|
// Broadcast only after tools are actually loaded into the cache.
|
|
@@ -1452,8 +1546,11 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1452
1546
|
client
|
|
1453
1547
|
.listPrompts({}, initRequestOptions || requestOptions)
|
|
1454
1548
|
.then((prompts) => {
|
|
1549
|
+
const target = liveServerInfo();
|
|
1550
|
+
if (!target)
|
|
1551
|
+
return;
|
|
1455
1552
|
logger.log(`Successfully listed ${prompts.prompts.length} prompts for server: ${name}`);
|
|
1456
|
-
updateServerPromptsCache(
|
|
1553
|
+
updateServerPromptsCache(target, prompts.prompts);
|
|
1457
1554
|
broadcastPromptListChanged();
|
|
1458
1555
|
})
|
|
1459
1556
|
.catch((error) => {
|
|
@@ -1468,8 +1565,11 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1468
1565
|
client
|
|
1469
1566
|
.listResources({}, initRequestOptions || requestOptions)
|
|
1470
1567
|
.then((resources) => {
|
|
1568
|
+
const target = liveServerInfo();
|
|
1569
|
+
if (!target)
|
|
1570
|
+
return;
|
|
1471
1571
|
logger.log(`Successfully listed ${resources.resources.length} resources for server: ${name}`);
|
|
1472
|
-
updateServerResourcesCache(
|
|
1572
|
+
updateServerResourcesCache(target, resources.resources);
|
|
1473
1573
|
broadcastResourceListChanged();
|
|
1474
1574
|
})
|
|
1475
1575
|
.catch((error) => {
|
|
@@ -1481,18 +1581,24 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1481
1581
|
});
|
|
1482
1582
|
}
|
|
1483
1583
|
if (!dataError) {
|
|
1484
|
-
|
|
1485
|
-
|
|
1584
|
+
info.status = 'connected';
|
|
1585
|
+
info.error = null;
|
|
1486
1586
|
// Set up keep-alive ping for SSE connections via shared service
|
|
1487
|
-
setupServerKeepAlive(
|
|
1587
|
+
setupServerKeepAlive(info, expandedConf);
|
|
1488
1588
|
}
|
|
1489
1589
|
else {
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
setupServerKeepAlive(
|
|
1590
|
+
info.status = 'disconnected';
|
|
1591
|
+
info.error = `Failed to list data: ${formatErrorForLogging(dataError)}`;
|
|
1592
|
+
setupServerKeepAlive(info, expandedConf);
|
|
1493
1593
|
}
|
|
1494
1594
|
})
|
|
1495
1595
|
.catch(async (error) => {
|
|
1596
|
+
const info = liveServerInfo();
|
|
1597
|
+
if (!info) {
|
|
1598
|
+
logger.log(`Discarding superseded connection failure for server: ${name}`);
|
|
1599
|
+
closeServerRuntime(serverInfo);
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1496
1602
|
// Check if this is an OAuth authorization error
|
|
1497
1603
|
const isOAuthError = error?.message?.includes('OAuth authorization required') ||
|
|
1498
1604
|
error?.message?.includes('Authorization required');
|
|
@@ -1501,10 +1607,10 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1501
1607
|
// and stored the authorization URL in serverInfo.oauth
|
|
1502
1608
|
logger.log(`OAuth authorization required for server ${name}. Status should be set to 'oauth_required'.`);
|
|
1503
1609
|
// Make sure status is set correctly
|
|
1504
|
-
if (
|
|
1505
|
-
|
|
1610
|
+
if (info.status !== 'oauth_required') {
|
|
1611
|
+
info.status = 'oauth_required';
|
|
1506
1612
|
}
|
|
1507
|
-
|
|
1613
|
+
info.error = null;
|
|
1508
1614
|
}
|
|
1509
1615
|
else {
|
|
1510
1616
|
logger.error('Failed to connect client for server', {
|
|
@@ -1512,9 +1618,9 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1512
1618
|
error: summarizeErrorForLogging(error),
|
|
1513
1619
|
});
|
|
1514
1620
|
// Other connection errors
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
setupServerKeepAlive(
|
|
1621
|
+
info.status = 'disconnected';
|
|
1622
|
+
info.error = `Failed to connect: ${formatErrorForLogging(error)}`;
|
|
1623
|
+
setupServerKeepAlive(info, expandedConf);
|
|
1518
1624
|
}
|
|
1519
1625
|
});
|
|
1520
1626
|
logger.log(`Initialized client for server: ${name}`);
|
|
@@ -1596,16 +1702,47 @@ export const getServersInfo = async (page, limit, user) => {
|
|
|
1596
1702
|
const filterServerInfos = shouldApplyUserFilter && dataService.filterData
|
|
1597
1703
|
? dataService.filterData(filteredServerInfos, user)
|
|
1598
1704
|
: filteredServerInfos;
|
|
1599
|
-
const
|
|
1705
|
+
const principal = (user ||
|
|
1706
|
+
UserContextService.getInstance().getCurrentUser());
|
|
1707
|
+
const presentedInfos = await Promise.all(filterServerInfos.map(async (info) => {
|
|
1708
|
+
if (!hasCredentialTemplate(info.config) ||
|
|
1709
|
+
info.enabled === false ||
|
|
1710
|
+
!principal?.username ||
|
|
1711
|
+
principal.credentialEligible === false)
|
|
1712
|
+
return info;
|
|
1713
|
+
try {
|
|
1714
|
+
const lease = await principalRuntimes.acquire(info.name, principal);
|
|
1715
|
+
try {
|
|
1716
|
+
return { ...lease.info, config: info.config };
|
|
1717
|
+
}
|
|
1718
|
+
finally {
|
|
1719
|
+
lease.release();
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
catch (error) {
|
|
1723
|
+
return {
|
|
1724
|
+
...info,
|
|
1725
|
+
error: error instanceof CredentialBindingError
|
|
1726
|
+
? error.message
|
|
1727
|
+
: 'Unable to resolve personal credentials',
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
}));
|
|
1731
|
+
const infos = presentedInfos
|
|
1600
1732
|
.filter((info) => requestedServerNames.has(info.name)) // Only include requested servers
|
|
1601
1733
|
.map(({ name, version, instructions, owner, visibility, status, tools, prompts, resources, createTime, error, oauth, }) => {
|
|
1602
1734
|
const serverConfig = allServers.find((server) => server.name === name);
|
|
1603
1735
|
const enabled = serverConfig ? serverConfig.enabled !== false : true;
|
|
1604
1736
|
const resolvedType = inferServerType(serverConfig);
|
|
1605
1737
|
const oauthConnected = Boolean(serverConfig?.oauth?.accessToken || serverConfig?.oauth?.refreshToken);
|
|
1606
|
-
// Add enabled status and custom description to each tool
|
|
1738
|
+
// Add enabled status and custom description to each tool.
|
|
1739
|
+
// toggleTool stores under whatever key the caller passed (prefixed,
|
|
1740
|
+
// as the dashboard sends, or bare, as a direct API call may) with no
|
|
1741
|
+
// normalization — check both so the dashboard reflects a disable set
|
|
1742
|
+
// either way instead of always showing enabled:true for one of them.
|
|
1607
1743
|
const toolsWithEnabled = tools.map((tool) => {
|
|
1608
|
-
const
|
|
1744
|
+
const bareToolName = normalizeToolNameForServer(name, tool.name);
|
|
1745
|
+
const toolConfig = serverConfig?.tools?.[tool.name] ?? serverConfig?.tools?.[bareToolName];
|
|
1609
1746
|
return buildToolWithDescriptionMetadata(tool, toolConfig);
|
|
1610
1747
|
});
|
|
1611
1748
|
const promptsWithEnabled = prompts.map((prompt) => {
|
|
@@ -1653,6 +1790,7 @@ export const getServersInfo = async (page, limit, user) => {
|
|
|
1653
1790
|
config: resolvedType || serverConfig?.description || serverConfig?.command
|
|
1654
1791
|
? {
|
|
1655
1792
|
...(resolvedType ? { type: resolvedType } : {}),
|
|
1793
|
+
...(hasCredentialTemplate(serverConfig) ? { credentialTemplate: validateCredentialTemplate(serverConfig) } : {}),
|
|
1656
1794
|
...(serverConfig?.description ? { description: serverConfig.description } : {}),
|
|
1657
1795
|
// Expose command so the frontend can determine if reinstall is
|
|
1658
1796
|
// supported (npx/uvx only). This is not a secret — it's the
|
|
@@ -1679,6 +1817,7 @@ export const getServerByOAuthState = (state) => {
|
|
|
1679
1817
|
*/
|
|
1680
1818
|
export const reconnectServer = async (serverName) => {
|
|
1681
1819
|
logger.log(`Reconnecting server: ${serverName}`);
|
|
1820
|
+
principalRuntimes.invalidate({ serverName });
|
|
1682
1821
|
const serverInfo = getServerByName(serverName);
|
|
1683
1822
|
if (!serverInfo) {
|
|
1684
1823
|
throw new Error(`Server not found: ${serverName}`);
|
|
@@ -1757,7 +1896,11 @@ const filterToolsByConfig = async (serverName, tools) => {
|
|
|
1757
1896
|
return tools;
|
|
1758
1897
|
}
|
|
1759
1898
|
return tools.filter((tool) => {
|
|
1760
|
-
|
|
1899
|
+
// toggleTool stores the disable under whatever key string the caller used
|
|
1900
|
+
// (bare upstream name or server-prefixed name, never normalized) — check
|
|
1901
|
+
// both, same as findToolOnServer's execution-time gate.
|
|
1902
|
+
const bareToolName = normalizeToolNameForServer(serverName, tool.name);
|
|
1903
|
+
const toolConfig = serverConfig.tools?.[tool.name] ?? serverConfig.tools?.[bareToolName];
|
|
1761
1904
|
// If tool is not in config, it's enabled by default
|
|
1762
1905
|
return toolConfig?.enabled !== false;
|
|
1763
1906
|
});
|
|
@@ -1804,6 +1947,7 @@ const classifyUnavailableReason = (qualifiedName) => {
|
|
|
1804
1947
|
};
|
|
1805
1948
|
// Add new server
|
|
1806
1949
|
export const addServer = async (name, config) => {
|
|
1950
|
+
validateCredentialTemplate(config);
|
|
1807
1951
|
const server = { name, ...config };
|
|
1808
1952
|
const result = await getServerDao().create(server);
|
|
1809
1953
|
if (result) {
|
|
@@ -1824,6 +1968,7 @@ export const removeServer = async (name) => {
|
|
|
1824
1968
|
// via npx / npm exec outlives the request and becomes an unkillable orphan
|
|
1825
1969
|
// that leaks memory until the container is restarted.
|
|
1826
1970
|
closeServer(name);
|
|
1971
|
+
await deleteCredentialBindings({ serverName: name });
|
|
1827
1972
|
try {
|
|
1828
1973
|
await removeServerToolEmbeddings(name);
|
|
1829
1974
|
}
|
|
@@ -1836,6 +1981,7 @@ export const removeServer = async (name) => {
|
|
|
1836
1981
|
// Add or update server (supports overriding existing servers for MCPB)
|
|
1837
1982
|
export const addOrUpdateServer = async (name, config, allowOverride = false) => {
|
|
1838
1983
|
try {
|
|
1984
|
+
validateCredentialTemplate(config);
|
|
1839
1985
|
const exists = await getServerDao().exists(name);
|
|
1840
1986
|
if (exists && !allowOverride) {
|
|
1841
1987
|
return { success: false, message: 'Server name already exists' };
|
|
@@ -1883,7 +2029,14 @@ function checkAuthError(result) {
|
|
|
1883
2029
|
}
|
|
1884
2030
|
}
|
|
1885
2031
|
}
|
|
2032
|
+
const clearIdleShutdown = (serverInfo) => {
|
|
2033
|
+
if (serverInfo.idleTimeoutId) {
|
|
2034
|
+
clearTimeout(serverInfo.idleTimeoutId);
|
|
2035
|
+
serverInfo.idleTimeoutId = undefined;
|
|
2036
|
+
}
|
|
2037
|
+
};
|
|
1886
2038
|
const closeServerRuntime = (serverInfo) => {
|
|
2039
|
+
clearIdleShutdown(serverInfo);
|
|
1887
2040
|
if (serverInfo.keepAliveIntervalId) {
|
|
1888
2041
|
clearInterval(serverInfo.keepAliveIntervalId);
|
|
1889
2042
|
serverInfo.keepAliveIntervalId = undefined;
|
|
@@ -1917,6 +2070,7 @@ const closeServerRuntime = (serverInfo) => {
|
|
|
1917
2070
|
// Close server client and transport (keeps the entry in serverInfos; the next
|
|
1918
2071
|
// initializeClientsFromSettings rebuild drops it if it is no longer configured).
|
|
1919
2072
|
export function closeServer(name) {
|
|
2073
|
+
principalRuntimes.invalidate({ serverName: name });
|
|
1920
2074
|
const serverInfo = serverInfos.find((serverInfo) => serverInfo.name === name);
|
|
1921
2075
|
if (serverInfo) {
|
|
1922
2076
|
closeServerRuntime(serverInfo);
|
|
@@ -1931,10 +2085,9 @@ export function closeServer(name) {
|
|
|
1931
2085
|
* the server offers and trigger a cold-start on the next tool call.
|
|
1932
2086
|
*/
|
|
1933
2087
|
const shutdownOnDemandServer = (serverInfo) => {
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
}
|
|
2088
|
+
// Discovery priming may finish while a tool call is using the same child.
|
|
2089
|
+
if (serverInfo.activeToolCalls)
|
|
2090
|
+
return;
|
|
1938
2091
|
// Close runtime (kills process) but intentionally keep tools/prompts/resources
|
|
1939
2092
|
closeServerRuntime(serverInfo);
|
|
1940
2093
|
serverInfo.status = 'disconnected';
|
|
@@ -1942,14 +2095,17 @@ const shutdownOnDemandServer = (serverInfo) => {
|
|
|
1942
2095
|
`${serverInfo.tools.length} tools cached for next wake-up`);
|
|
1943
2096
|
};
|
|
1944
2097
|
/**
|
|
1945
|
-
*
|
|
1946
|
-
* Call this after every successful tool invocation.
|
|
2098
|
+
* Starts the idle period once the last active call has finished.
|
|
1947
2099
|
*/
|
|
1948
2100
|
const scheduleIdleShutdown = (serverInfo) => {
|
|
1949
|
-
|
|
2101
|
+
clearIdleShutdown(serverInfo);
|
|
2102
|
+
if (!serverInfo.config?.startOnDemand ||
|
|
2103
|
+
!isStdioServer(serverInfo.config) ||
|
|
2104
|
+
hasCredentialTemplate(serverInfo.config) ||
|
|
2105
|
+
serverInfo.status !== 'connected' ||
|
|
2106
|
+
!serverInfo.client ||
|
|
2107
|
+
serverInfo.activeToolCalls) {
|
|
1950
2108
|
return;
|
|
1951
|
-
if (serverInfo.idleTimeoutId) {
|
|
1952
|
-
clearTimeout(serverInfo.idleTimeoutId);
|
|
1953
2109
|
}
|
|
1954
2110
|
const idleMs = serverInfo.config.idleTimeoutMs ?? 300000; // default 5 minutes
|
|
1955
2111
|
serverInfo.idleTimeoutId = setTimeout(() => {
|
|
@@ -1958,6 +2114,19 @@ const scheduleIdleShutdown = (serverInfo) => {
|
|
|
1958
2114
|
}
|
|
1959
2115
|
}, idleMs);
|
|
1960
2116
|
};
|
|
2117
|
+
const beginOnDemandToolCall = (serverInfo) => {
|
|
2118
|
+
if (!serverInfo.config?.startOnDemand ||
|
|
2119
|
+
!isStdioServer(serverInfo.config) ||
|
|
2120
|
+
hasCredentialTemplate(serverInfo.config)) {
|
|
2121
|
+
return undefined;
|
|
2122
|
+
}
|
|
2123
|
+
clearIdleShutdown(serverInfo);
|
|
2124
|
+
serverInfo.activeToolCalls = (serverInfo.activeToolCalls ?? 0) + 1;
|
|
2125
|
+
return () => {
|
|
2126
|
+
serverInfo.activeToolCalls = serverInfo.activeToolCalls - 1;
|
|
2127
|
+
scheduleIdleShutdown(serverInfo);
|
|
2128
|
+
};
|
|
2129
|
+
};
|
|
1961
2130
|
/**
|
|
1962
2131
|
* Ensures an on-demand server is connected before a tool call proceeds.
|
|
1963
2132
|
*
|
|
@@ -2094,6 +2263,7 @@ const isStdioServer = (conf) => !!conf && (conf.type === 'stdio' || (!conf.type
|
|
|
2094
2263
|
*/
|
|
2095
2264
|
const primeOnDemandServers = () => {
|
|
2096
2265
|
const targets = serverInfos.filter((si) => si.config?.startOnDemand === true &&
|
|
2266
|
+
!hasCredentialTemplate(si.config) &&
|
|
2097
2267
|
isStdioServer(si.config) &&
|
|
2098
2268
|
si.tools.length === 0 &&
|
|
2099
2269
|
si.enabled !== false);
|
|
@@ -2103,9 +2273,9 @@ const primeOnDemandServers = () => {
|
|
|
2103
2273
|
return Promise.allSettled(targets.map(async (si) => {
|
|
2104
2274
|
try {
|
|
2105
2275
|
await ensureServerReady(si);
|
|
2106
|
-
// Sleep the child
|
|
2276
|
+
// Sleep the child unless a tool call is using it; keep the cached tool list.
|
|
2107
2277
|
shutdownOnDemandServer(si);
|
|
2108
|
-
logger.log('On-demand server
|
|
2278
|
+
logger.log('On-demand server tool cache primed');
|
|
2109
2279
|
}
|
|
2110
2280
|
catch (error) {
|
|
2111
2281
|
logger.warn('Failed to prime on-demand server', {
|
|
@@ -2322,20 +2492,54 @@ const resolveNameFromGroup = (name, serverName, serverConfig) => {
|
|
|
2322
2492
|
return replacePrefixedServerName(name, getExposedServerName(serverName, serverConfig), serverName);
|
|
2323
2493
|
};
|
|
2324
2494
|
const findToolOnServer = (serverInfo, toolName, allowRawName) => {
|
|
2325
|
-
|
|
2495
|
+
const tool = serverInfo.tools.find((tool) => tool.name === toolName ||
|
|
2326
2496
|
(allowRawName && normalizeToolNameForServer(serverInfo.name, tool.name) === toolName));
|
|
2497
|
+
if (!tool) {
|
|
2498
|
+
return undefined;
|
|
2499
|
+
}
|
|
2500
|
+
// A tool disabled via POST /api/servers/:server/tools/:tool/toggle must be
|
|
2501
|
+
// unresolvable for execution, not just hidden from tools/list — otherwise a
|
|
2502
|
+
// caller who already knows the tool name can invoke it regardless of the
|
|
2503
|
+
// disabled flag.
|
|
2504
|
+
//
|
|
2505
|
+
// The runtime tool cache (serverInfo.tools, built by normalizeToolForCache)
|
|
2506
|
+
// never carries an `enabled` field — that field only exists on the
|
|
2507
|
+
// *projection* built for the dashboard/API (buildToolWithDescriptionMetadata),
|
|
2508
|
+
// which is a different object. So the persisted per-tool config must be
|
|
2509
|
+
// consulted directly here.
|
|
2510
|
+
//
|
|
2511
|
+
// toggleTool (serverController.ts) stores `tools[toolName] = ...` using
|
|
2512
|
+
// whatever string the caller passed as :toolName, with no normalization.
|
|
2513
|
+
// The dashboard sends the already-prefixed `tool.name`, matching what
|
|
2514
|
+
// getServersInfo's toolsWithEnabled already reads — but any other caller
|
|
2515
|
+
// (REST API used directly, scripts, docs examples) can just as validly
|
|
2516
|
+
// pass the bare upstream name, which then never matches a prefixed-only
|
|
2517
|
+
// lookup. Check both so a disable takes effect regardless of which form
|
|
2518
|
+
// was used to set it.
|
|
2519
|
+
const bareToolName = normalizeToolNameForServer(serverInfo.name, tool.name);
|
|
2520
|
+
const toolConfig = serverInfo.config?.tools?.[tool.name] ?? serverInfo.config?.tools?.[bareToolName];
|
|
2521
|
+
if (toolConfig && toolConfig.enabled === false) {
|
|
2522
|
+
return undefined;
|
|
2523
|
+
}
|
|
2524
|
+
return tool;
|
|
2327
2525
|
};
|
|
2328
2526
|
const assertToolAvailableForRoute = (tool, appsRouteContext) => {
|
|
2329
2527
|
if (isAppOnlyTool(tool) && !appsRouteContext.enabled) {
|
|
2330
2528
|
throw new Error(`Tool '${tool.name}' is only available to MCP Apps`);
|
|
2331
2529
|
}
|
|
2332
2530
|
};
|
|
2333
|
-
const resolveToolInGroup = async (group, toolName
|
|
2531
|
+
const resolveToolInGroup = async (group, toolName) => {
|
|
2334
2532
|
const lookupGroup = getGroupLookupName(group);
|
|
2335
2533
|
if (!lookupGroup) {
|
|
2336
2534
|
return undefined;
|
|
2337
2535
|
}
|
|
2338
2536
|
const { filteredServerInfos, serverConfigsByName } = await getFilteredServerInfosForGroup(lookupGroup);
|
|
2537
|
+
// Accept raw names only on single-upstream routes, even without client
|
|
2538
|
+
// capabilities after session rebuild. Count configured servers as well so
|
|
2539
|
+
// a disabled or disconnected peer cannot make a group appear unambiguous.
|
|
2540
|
+
const allowRawName = !isSmartRoutingGroup(group) &&
|
|
2541
|
+
serverConfigsByName.size <= 1 &&
|
|
2542
|
+
filteredServerInfos.length === 1;
|
|
2339
2543
|
for (const serverInfo of filteredServerInfos) {
|
|
2340
2544
|
// A disconnected on-demand server may still have a cached tool list from a
|
|
2341
2545
|
// previous wake-up; allow it through so it can be selected and woken by the
|
|
@@ -2402,7 +2606,7 @@ const projectToolForDownstream = (serverName, tool, appsRouteContext, serverConf
|
|
|
2402
2606
|
name: projectNameForGroup(projectedTool.name, serverName, serverConfig),
|
|
2403
2607
|
};
|
|
2404
2608
|
};
|
|
2405
|
-
|
|
2609
|
+
const handleListToolsRequestImpl = async (_, extra) => {
|
|
2406
2610
|
const sessionId = extra.sessionId || '';
|
|
2407
2611
|
const group = getGroup(sessionId);
|
|
2408
2612
|
logger.log(`Handling ListToolsRequest for group: ${group}`);
|
|
@@ -2446,7 +2650,7 @@ export const handleListToolsRequest = async (_, extra) => {
|
|
|
2446
2650
|
tools: allTools,
|
|
2447
2651
|
};
|
|
2448
2652
|
};
|
|
2449
|
-
|
|
2653
|
+
const handleCallToolRequestImpl = async (request, extra) => {
|
|
2450
2654
|
logger.log('Handling CallToolRequest for tool', summarizeToolRequestForLogging(request.params));
|
|
2451
2655
|
const startTime = Date.now();
|
|
2452
2656
|
const activityLogger = getActivityLoggingService();
|
|
@@ -2478,6 +2682,7 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2478
2682
|
const keyName = bearerKeyContext.keyName || extra?.keyName || undefined;
|
|
2479
2683
|
const sourceIp = requestContextService.getRequestContext()?.remoteAddress || undefined;
|
|
2480
2684
|
let hostedReservation = null;
|
|
2685
|
+
let releaseOnDemandToolCall;
|
|
2481
2686
|
const reserveHostedIfNeeded = async (serverName, toolName) => {
|
|
2482
2687
|
const hostedAuth = requestContextService.getHostedAuthContext();
|
|
2483
2688
|
assertHostedToolAllowed(hostedAuth, serverName, toolName);
|
|
@@ -2527,7 +2732,7 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2527
2732
|
targetServerInfo = getVisibleServerByName(extra.server);
|
|
2528
2733
|
}
|
|
2529
2734
|
else if (getGroupLookupName(group)) {
|
|
2530
|
-
const groupTool = await resolveToolInGroup(group, toolName
|
|
2735
|
+
const groupTool = await resolveToolInGroup(group, toolName);
|
|
2531
2736
|
if (groupTool) {
|
|
2532
2737
|
targetServerInfo = groupTool.serverInfo;
|
|
2533
2738
|
targetToolName = groupTool.toolName;
|
|
@@ -2547,6 +2752,7 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2547
2752
|
}
|
|
2548
2753
|
// If the target is an on-demand server that is not yet running, wake it up now.
|
|
2549
2754
|
// Concurrent callers are serialised via ensureServerReady's singleton promise.
|
|
2755
|
+
releaseOnDemandToolCall = beginOnDemandToolCall(targetServerInfo);
|
|
2550
2756
|
if (targetServerInfo.config?.startOnDemand && targetServerInfo.status !== 'connected') {
|
|
2551
2757
|
await ensureServerReady(targetServerInfo);
|
|
2552
2758
|
// Re-read status from the mutated serverInfo after async spawn
|
|
@@ -2626,6 +2832,7 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2626
2832
|
keyId,
|
|
2627
2833
|
keyName,
|
|
2628
2834
|
sourceIp,
|
|
2835
|
+
hasCredentialTemplate: hasCredentialTemplate(targetServerInfo.config),
|
|
2629
2836
|
});
|
|
2630
2837
|
return await maybeCompressToolResult({
|
|
2631
2838
|
content: [
|
|
@@ -2643,7 +2850,7 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2643
2850
|
// Call the tool on the target server (MCP servers)
|
|
2644
2851
|
// For servers with perSessionClient: true, use a per-session dedicated client
|
|
2645
2852
|
let isolatedCtx;
|
|
2646
|
-
if (targetServerInfo.config?.perSessionClient && sessionId) {
|
|
2853
|
+
if (targetServerInfo.config?.perSessionClient && !hasCredentialTemplate(targetServerInfo.config) && sessionId) {
|
|
2647
2854
|
const isolated = await getOrCreateIsolatedClient(sessionId, targetServerInfo);
|
|
2648
2855
|
isolatedCtx = { sessionId, client: isolated.client, transport: isolated.transport };
|
|
2649
2856
|
}
|
|
@@ -2674,8 +2881,6 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2674
2881
|
toolName: cleanToolName,
|
|
2675
2882
|
result: summarizeToolResultForLogging(result),
|
|
2676
2883
|
});
|
|
2677
|
-
// Reset idle-shutdown timer for on-demand servers after each successful call
|
|
2678
|
-
scheduleIdleShutdown(targetServerInfo);
|
|
2679
2884
|
// Log successful activity
|
|
2680
2885
|
const duration = Date.now() - startTime;
|
|
2681
2886
|
await activityLogger.logToolCall({
|
|
@@ -2690,6 +2895,7 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2690
2895
|
keyId,
|
|
2691
2896
|
keyName,
|
|
2692
2897
|
sourceIp,
|
|
2898
|
+
hasCredentialTemplate: hasCredentialTemplate(targetServerInfo.config),
|
|
2693
2899
|
errorMessage: result.isError ? 'Tool returned error response' : undefined,
|
|
2694
2900
|
});
|
|
2695
2901
|
return await maybeCompressToolResult(result, {
|
|
@@ -2708,7 +2914,7 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2708
2914
|
// (assertToolAvailableForRoute below still gates on appsRouteContext.enabled).
|
|
2709
2915
|
const singleServerAppsRoute = !!appsRouteContext.serverInfo;
|
|
2710
2916
|
const groupTool = !singleServerAppsRoute && lookupGroup
|
|
2711
|
-
? await resolveToolInGroup(
|
|
2917
|
+
? await resolveToolInGroup(group, request.params.name)
|
|
2712
2918
|
: undefined;
|
|
2713
2919
|
const serverInfo = singleServerAppsRoute
|
|
2714
2920
|
? appsRouteContext.serverInfo
|
|
@@ -2723,6 +2929,7 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2723
2929
|
// Wake the on-demand server before invoking the tool. It may be asleep with
|
|
2724
2930
|
// a cached tool list (visible above) but no live client. Mirrors the
|
|
2725
2931
|
// $smart call_tool path. See #1029.
|
|
2932
|
+
releaseOnDemandToolCall = beginOnDemandToolCall(serverInfo);
|
|
2726
2933
|
if (serverInfo.config?.startOnDemand && serverInfo.status !== 'connected') {
|
|
2727
2934
|
await ensureServerReady(serverInfo);
|
|
2728
2935
|
}
|
|
@@ -2789,6 +2996,7 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2789
2996
|
keyId,
|
|
2790
2997
|
keyName,
|
|
2791
2998
|
sourceIp,
|
|
2999
|
+
hasCredentialTemplate: hasCredentialTemplate(serverInfo.config),
|
|
2792
3000
|
});
|
|
2793
3001
|
return await maybeCompressToolResult({
|
|
2794
3002
|
content: [
|
|
@@ -2806,7 +3014,7 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2806
3014
|
// Handle MCP servers
|
|
2807
3015
|
// For servers with perSessionClient: true, use a per-session dedicated client
|
|
2808
3016
|
let isolatedCtx;
|
|
2809
|
-
if (serverInfo.config?.perSessionClient && sessionId) {
|
|
3017
|
+
if (serverInfo.config?.perSessionClient && !hasCredentialTemplate(serverInfo.config) && sessionId) {
|
|
2810
3018
|
const isolated = await getOrCreateIsolatedClient(sessionId, serverInfo);
|
|
2811
3019
|
isolatedCtx = { sessionId, client: isolated.client, transport: isolated.transport };
|
|
2812
3020
|
}
|
|
@@ -2826,8 +3034,6 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2826
3034
|
toolName: cleanToolName,
|
|
2827
3035
|
result: summarizeToolResultForLogging(result),
|
|
2828
3036
|
});
|
|
2829
|
-
// Reset idle-shutdown timer for on-demand servers after each successful call
|
|
2830
|
-
scheduleIdleShutdown(serverInfo);
|
|
2831
3037
|
// Log successful activity
|
|
2832
3038
|
const duration = Date.now() - startTime;
|
|
2833
3039
|
await activityLogger.logToolCall({
|
|
@@ -2842,6 +3048,7 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2842
3048
|
keyId,
|
|
2843
3049
|
keyName,
|
|
2844
3050
|
sourceIp,
|
|
3051
|
+
hasCredentialTemplate: hasCredentialTemplate(serverInfo.config),
|
|
2845
3052
|
errorMessage: result.isError ? 'Tool returned error response' : undefined,
|
|
2846
3053
|
});
|
|
2847
3054
|
return await maybeCompressToolResult(result, {
|
|
@@ -2880,6 +3087,7 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2880
3087
|
keyId,
|
|
2881
3088
|
keyName,
|
|
2882
3089
|
sourceIp,
|
|
3090
|
+
hasCredentialTemplate: hasCredentialTemplate(serverInfo?.config),
|
|
2883
3091
|
// The reason is an internal diagnostic; the caller-facing message stays
|
|
2884
3092
|
// identical for hidden and nonexistent targets.
|
|
2885
3093
|
errorMessage: unavailable
|
|
@@ -2899,8 +3107,11 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2899
3107
|
isError: true,
|
|
2900
3108
|
};
|
|
2901
3109
|
}
|
|
3110
|
+
finally {
|
|
3111
|
+
releaseOnDemandToolCall?.();
|
|
3112
|
+
}
|
|
2902
3113
|
};
|
|
2903
|
-
|
|
3114
|
+
const handleGetPromptRequestImpl = async (request, extra) => {
|
|
2904
3115
|
try {
|
|
2905
3116
|
const { name, arguments: promptArgs } = request.params;
|
|
2906
3117
|
const sessionId = extra?.sessionId || '';
|
|
@@ -2989,7 +3200,7 @@ export const handleGetPromptRequest = async (request, extra) => {
|
|
|
2989
3200
|
};
|
|
2990
3201
|
}
|
|
2991
3202
|
};
|
|
2992
|
-
|
|
3203
|
+
const handleListPromptsRequestImpl = async (_, extra) => {
|
|
2993
3204
|
const sessionId = extra.sessionId || '';
|
|
2994
3205
|
const group = getGroup(sessionId);
|
|
2995
3206
|
const lookupGroup = getGroupLookupName(group);
|
|
@@ -3033,7 +3244,7 @@ export const handleListPromptsRequest = async (_, extra) => {
|
|
|
3033
3244
|
prompts: allPrompts,
|
|
3034
3245
|
};
|
|
3035
3246
|
};
|
|
3036
|
-
|
|
3247
|
+
const handleListResourcesRequestImpl = async (_, extra) => {
|
|
3037
3248
|
const sessionId = extra.sessionId || '';
|
|
3038
3249
|
const group = getGroup(sessionId);
|
|
3039
3250
|
const lookupGroup = getGroupLookupName(group);
|
|
@@ -3078,7 +3289,7 @@ export const handleListResourcesRequest = async (_, extra) => {
|
|
|
3078
3289
|
resources: allResources,
|
|
3079
3290
|
};
|
|
3080
3291
|
};
|
|
3081
|
-
|
|
3292
|
+
const handleListResourceTemplatesRequestImpl = async (_, extra) => {
|
|
3082
3293
|
const sessionId = extra.sessionId || '';
|
|
3083
3294
|
const group = getGroup(sessionId);
|
|
3084
3295
|
const lookupGroup = getGroupLookupName(group);
|
|
@@ -3101,7 +3312,7 @@ export const handleListResourceTemplatesRequest = async (_, extra) => {
|
|
|
3101
3312
|
resourceTemplates: results.flatMap((result) => result.status === 'fulfilled' ? result.value : []),
|
|
3102
3313
|
};
|
|
3103
3314
|
};
|
|
3104
|
-
|
|
3315
|
+
const handleReadResourceRequestImpl = async (request, extra) => {
|
|
3105
3316
|
try {
|
|
3106
3317
|
const { uri } = request.params;
|
|
3107
3318
|
const sessionId = extra.sessionId || '';
|
|
@@ -3198,6 +3409,197 @@ export const handleReadResourceRequest = async (request, extra) => {
|
|
|
3198
3409
|
};
|
|
3199
3410
|
}
|
|
3200
3411
|
};
|
|
3412
|
+
// Personal runtimes never enter serverInfos, config exports, or shared embedding caches.
|
|
3413
|
+
const createPrincipalRuntime = async (name, resolvedConfig) => {
|
|
3414
|
+
const info = {
|
|
3415
|
+
name,
|
|
3416
|
+
owner: resolvedConfig.owner,
|
|
3417
|
+
visibility: resolvedConfig.visibility,
|
|
3418
|
+
sharedWithUsers: resolvedConfig.sharedWithUsers,
|
|
3419
|
+
enabled: true,
|
|
3420
|
+
config: resolvedConfig,
|
|
3421
|
+
options: resolvedConfig.options,
|
|
3422
|
+
status: 'connecting',
|
|
3423
|
+
error: null,
|
|
3424
|
+
tools: [],
|
|
3425
|
+
prompts: [],
|
|
3426
|
+
resources: [],
|
|
3427
|
+
createTime: Date.now(),
|
|
3428
|
+
};
|
|
3429
|
+
const safeMethods = new Set([
|
|
3430
|
+
'callTool',
|
|
3431
|
+
'getPrompt',
|
|
3432
|
+
'readResource',
|
|
3433
|
+
'listTools',
|
|
3434
|
+
'listPrompts',
|
|
3435
|
+
'listResources',
|
|
3436
|
+
'listResourceTemplates',
|
|
3437
|
+
]);
|
|
3438
|
+
const protectClient = (client) => new Proxy(client, {
|
|
3439
|
+
get(target, property) {
|
|
3440
|
+
const value = Reflect.get(target, property);
|
|
3441
|
+
if (typeof value !== 'function')
|
|
3442
|
+
return value;
|
|
3443
|
+
if (!safeMethods.has(String(property)))
|
|
3444
|
+
return value.bind(target);
|
|
3445
|
+
return async (...args) => {
|
|
3446
|
+
try {
|
|
3447
|
+
return await value.apply(target, args);
|
|
3448
|
+
}
|
|
3449
|
+
catch {
|
|
3450
|
+
info.status = 'disconnected';
|
|
3451
|
+
throw new CredentialBindingError(`Personal credential request failed for '${name}'. Check your binding in Credentials.`);
|
|
3452
|
+
}
|
|
3453
|
+
};
|
|
3454
|
+
},
|
|
3455
|
+
});
|
|
3456
|
+
try {
|
|
3457
|
+
if (resolvedConfig.type === 'openapi') {
|
|
3458
|
+
const client = new OpenAPIClient(resolvedConfig);
|
|
3459
|
+
await client.initialize();
|
|
3460
|
+
info.openApiClient = protectClient(client);
|
|
3461
|
+
info.tools = client
|
|
3462
|
+
.getTools()
|
|
3463
|
+
.map((tool) => ({
|
|
3464
|
+
...tool,
|
|
3465
|
+
name: `${name}${getNameSeparator()}${tool.name}`,
|
|
3466
|
+
inputSchema: cleanInputSchema(tool.inputSchema),
|
|
3467
|
+
}));
|
|
3468
|
+
}
|
|
3469
|
+
else {
|
|
3470
|
+
const transport = await createTransportFromConfig(name, resolvedConfig);
|
|
3471
|
+
const client = new Client({ name: `mcp-client-${name}`, version: '1.0.0' }, {
|
|
3472
|
+
capabilities: MCP_APPS_CAPABILITIES,
|
|
3473
|
+
jsonSchemaValidator: new ResilientJsonSchemaValidator(),
|
|
3474
|
+
});
|
|
3475
|
+
info.transport = transport;
|
|
3476
|
+
info.client = protectClient(client);
|
|
3477
|
+
await client.connect(transport, resolvedConfig.options || {});
|
|
3478
|
+
const capabilities = client.getServerCapabilities();
|
|
3479
|
+
if (capabilities?.tools)
|
|
3480
|
+
info.tools = (await client.listTools({}, resolvedConfig.options)).tools.map((tool) => normalizeToolForCache(name, tool));
|
|
3481
|
+
if (capabilities?.prompts)
|
|
3482
|
+
info.prompts = (await client.listPrompts({}, resolvedConfig.options)).prompts.map((prompt) => normalizePromptForCache(name, prompt));
|
|
3483
|
+
if (capabilities?.resources)
|
|
3484
|
+
info.resources = (await client.listResources({}, resolvedConfig.options)).resources.map(normalizeResourceForCache);
|
|
3485
|
+
info.version = client.getServerVersion()?.version;
|
|
3486
|
+
client.onclose = () => {
|
|
3487
|
+
info.status = 'disconnected';
|
|
3488
|
+
};
|
|
3489
|
+
}
|
|
3490
|
+
info.status = 'connected';
|
|
3491
|
+
return info;
|
|
3492
|
+
}
|
|
3493
|
+
catch {
|
|
3494
|
+
closeServerRuntime(info);
|
|
3495
|
+
throw new CredentialBindingError(`Unable to connect '${name}' with your personal credentials. Check your binding in Credentials.`);
|
|
3496
|
+
}
|
|
3497
|
+
};
|
|
3498
|
+
const principalRuntimes = new PrincipalRuntimeService(createPrincipalRuntime, closeServerRuntime);
|
|
3499
|
+
const withPrincipalServers = (handler, operation) => async (request, extra) => {
|
|
3500
|
+
if (!serverInfos.some((info) => hasCredentialTemplate(info.config)))
|
|
3501
|
+
return handler(request, extra);
|
|
3502
|
+
// Built-in content does not require an upstream credential.
|
|
3503
|
+
if (operation === 'prompt') {
|
|
3504
|
+
const builtin = await getBuiltinPromptDao().findByName(request.params?.name);
|
|
3505
|
+
if (builtin && builtin.enabled !== false)
|
|
3506
|
+
return handler(request, extra);
|
|
3507
|
+
}
|
|
3508
|
+
else if (operation === 'resource') {
|
|
3509
|
+
const builtin = await getBuiltinResourceDao().findByUri(request.params?.uri);
|
|
3510
|
+
if (builtin && builtin.enabled !== false)
|
|
3511
|
+
return handler(request, extra);
|
|
3512
|
+
}
|
|
3513
|
+
const group = RequestContextService.getInstance().getGroupContext() ||
|
|
3514
|
+
extra?.group ||
|
|
3515
|
+
getGroup(extra?.sessionId || '') ||
|
|
3516
|
+
undefined;
|
|
3517
|
+
const { filteredServerInfos: candidates, serverConfigsByName } = await getFilteredServerInfosForGroup(getGroupLookupName(group));
|
|
3518
|
+
const requestedName = ['call_tool', 'describe_tool'].includes(request?.params?.name)
|
|
3519
|
+
? request.params.arguments?.toolName
|
|
3520
|
+
: request?.params?.name;
|
|
3521
|
+
const target = candidates.find((info) => {
|
|
3522
|
+
const exposedName = getExposedServerName(info.name, serverConfigsByName.get(info.name));
|
|
3523
|
+
return (extra?.server === info.name ||
|
|
3524
|
+
(typeof requestedName === 'string' &&
|
|
3525
|
+
requestedName.startsWith(`${exposedName}${getNameSeparator()}`)));
|
|
3526
|
+
});
|
|
3527
|
+
// A call to a known target must not spawn or keep unrelated personal children alive.
|
|
3528
|
+
const selected = target && (operation === 'tool' || operation === 'prompt') ? [target] : candidates;
|
|
3529
|
+
const templated = selected.filter((info) => hasCredentialTemplate(info.config));
|
|
3530
|
+
if (!templated.length)
|
|
3531
|
+
return handler(request, extra);
|
|
3532
|
+
const user = UserContextService.getInstance().getCurrentUser();
|
|
3533
|
+
const releases = [];
|
|
3534
|
+
const scoped = new Map(serverInfos.map((info) => [info.name, info]));
|
|
3535
|
+
const failures = new Map();
|
|
3536
|
+
try {
|
|
3537
|
+
await Promise.all(templated.map(async (info) => {
|
|
3538
|
+
try {
|
|
3539
|
+
if (!user?.username ||
|
|
3540
|
+
user.credentialEligible === false ||
|
|
3541
|
+
RequestContextService.getInstance().getKeyKindContext() === 'system')
|
|
3542
|
+
throw missingCredentialError(info.name);
|
|
3543
|
+
const lease = await principalRuntimes.acquire(info.name, user);
|
|
3544
|
+
releases.push(lease.release);
|
|
3545
|
+
scoped.set(info.name, lease.info);
|
|
3546
|
+
}
|
|
3547
|
+
catch (error) {
|
|
3548
|
+
failures.set(info.name, error instanceof CredentialBindingError
|
|
3549
|
+
? error
|
|
3550
|
+
: new CredentialBindingError('Unable to resolve personal credentials'));
|
|
3551
|
+
}
|
|
3552
|
+
}));
|
|
3553
|
+
const requiredTarget = target ||
|
|
3554
|
+
(candidates.length === 1 && operation !== 'list' && requestedName !== 'search_tools'
|
|
3555
|
+
? templated[0]
|
|
3556
|
+
: undefined);
|
|
3557
|
+
const failure = requiredTarget ? failures.get(requiredTarget.name) : undefined;
|
|
3558
|
+
if (failure) {
|
|
3559
|
+
if (operation === 'resource')
|
|
3560
|
+
return {
|
|
3561
|
+
contents: [
|
|
3562
|
+
{ uri: request.params?.uri || '', mimeType: 'text/plain', text: failure.message },
|
|
3563
|
+
],
|
|
3564
|
+
};
|
|
3565
|
+
return { content: [{ type: 'text', text: failure.message }], isError: true };
|
|
3566
|
+
}
|
|
3567
|
+
// Dashboard/OpenAPI callers may address a tool by its raw name plus server.
|
|
3568
|
+
let resolvedRequest = request;
|
|
3569
|
+
if (operation === 'tool' &&
|
|
3570
|
+
extra?.server &&
|
|
3571
|
+
hasCredentialTemplate(scoped.get(extra.server)?.config)) {
|
|
3572
|
+
const prefix = `${extra.server}${getNameSeparator()}`;
|
|
3573
|
+
if (typeof requestedName === 'string' && !requestedName.startsWith(prefix)) {
|
|
3574
|
+
resolvedRequest =
|
|
3575
|
+
request.params.name === 'call_tool'
|
|
3576
|
+
? {
|
|
3577
|
+
...request,
|
|
3578
|
+
params: {
|
|
3579
|
+
...request.params,
|
|
3580
|
+
arguments: {
|
|
3581
|
+
...request.params.arguments,
|
|
3582
|
+
toolName: `${prefix}${requestedName}`,
|
|
3583
|
+
},
|
|
3584
|
+
},
|
|
3585
|
+
}
|
|
3586
|
+
: { ...request, params: { ...request.params, name: `${prefix}${requestedName}` } };
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
return await principalServerScope.run([...scoped.values()], () => handler(resolvedRequest, extra));
|
|
3590
|
+
}
|
|
3591
|
+
finally {
|
|
3592
|
+
for (const release of releases)
|
|
3593
|
+
release();
|
|
3594
|
+
}
|
|
3595
|
+
};
|
|
3596
|
+
export const handleListToolsRequest = withPrincipalServers(handleListToolsRequestImpl, 'list');
|
|
3597
|
+
export const handleCallToolRequest = withPrincipalServers(handleCallToolRequestImpl, 'tool');
|
|
3598
|
+
export const handleGetPromptRequest = withPrincipalServers(handleGetPromptRequestImpl, 'prompt');
|
|
3599
|
+
export const handleListPromptsRequest = withPrincipalServers(handleListPromptsRequestImpl, 'list');
|
|
3600
|
+
export const handleListResourcesRequest = withPrincipalServers(handleListResourcesRequestImpl, 'list');
|
|
3601
|
+
export const handleListResourceTemplatesRequest = withPrincipalServers(handleListResourceTemplatesRequestImpl, 'list');
|
|
3602
|
+
export const handleReadResourceRequest = withPrincipalServers(handleReadResourceRequestImpl, 'resource');
|
|
3201
3603
|
export const createMcpServer = (name, version, options) => {
|
|
3202
3604
|
const normalizedOptions = typeof options === 'string' ? { group: options } : (options ?? {});
|
|
3203
3605
|
// Determine server name based on routing type
|
|
@@ -3229,13 +3631,8 @@ export const createMcpServer = (name, version, options) => {
|
|
|
3229
3631
|
return server;
|
|
3230
3632
|
};
|
|
3231
3633
|
export const getFilteredServerInfosForGroup = async (group, options) => {
|
|
3232
|
-
// Resolve
|
|
3233
|
-
|
|
3234
|
-
// and applies filterData on groups). Groups don't carry a visibility field,
|
|
3235
|
-
// so admin-owned groups would be filtered out for non-admin users even though
|
|
3236
|
-
// bearer-key auth already authorized access and individual servers inside the
|
|
3237
|
-
// group may be public. Server-level filterData below still enforces per-server
|
|
3238
|
-
// visibility. Fix for #914.
|
|
3634
|
+
// Resolve raw groups so legacy routing remains compatible, then enforce explicit visibility.
|
|
3635
|
+
let matchedGroup = false;
|
|
3239
3636
|
let serverConfigs = [];
|
|
3240
3637
|
if (group) {
|
|
3241
3638
|
const groupDao = getGroupDao();
|
|
@@ -3247,6 +3644,10 @@ export const getFilteredServerInfosForGroup = async (group, options) => {
|
|
|
3247
3644
|
}
|
|
3248
3645
|
}
|
|
3249
3646
|
if (foundGroup) {
|
|
3647
|
+
matchedGroup = true;
|
|
3648
|
+
if (!canAccessGroupRoute(foundGroup)) {
|
|
3649
|
+
return { filteredServerInfos: [], serverConfigsByName: new Map() };
|
|
3650
|
+
}
|
|
3250
3651
|
serverConfigs = normalizeGroupServers(foundGroup.servers || []);
|
|
3251
3652
|
}
|
|
3252
3653
|
}
|
|
@@ -3262,7 +3663,7 @@ export const getFilteredServerInfosForGroup = async (group, options) => {
|
|
|
3262
3663
|
filteredServerInfos.push(serverInfo);
|
|
3263
3664
|
continue;
|
|
3264
3665
|
}
|
|
3265
|
-
if (
|
|
3666
|
+
if (!matchedGroup) {
|
|
3266
3667
|
if (serverInfo.name === group) {
|
|
3267
3668
|
filteredServerInfos.push(serverInfo);
|
|
3268
3669
|
}
|