@samanhappy/mcphub 1.1.2 → 1.1.4
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 +6 -0
- package/README.md +6 -0
- package/README.zh.md +4 -0
- package/dist/cli/http.js +54 -1
- package/dist/cli/http.js.map +1 -1
- package/dist/controllers/mcpbController.js +6 -2
- package/dist/controllers/mcpbController.js.map +1 -1
- package/dist/controllers/oauthCallbackController.js +11 -16
- package/dist/controllers/oauthCallbackController.js.map +1 -1
- package/dist/controllers/oauthServerController.js +10 -7
- package/dist/controllers/oauthServerController.js.map +1 -1
- package/dist/controllers/personalOAuthController.js +119 -0
- package/dist/controllers/personalOAuthController.js.map +1 -0
- package/dist/controllers/serverController.js +17 -2
- package/dist/controllers/serverController.js.map +1 -1
- package/dist/controllers/smartRoutingController.js +4 -4
- package/dist/controllers/smartRoutingController.js.map +1 -1
- package/dist/dao/CredentialBindingDao.js +8 -0
- package/dist/dao/CredentialBindingDao.js.map +1 -1
- package/dist/dao/CredentialBindingDaoDbImpl.js +14 -0
- package/dist/dao/CredentialBindingDaoDbImpl.js.map +1 -1
- package/dist/dao/SystemConfigDaoDbImpl.js +3 -0
- package/dist/dao/SystemConfigDaoDbImpl.js.map +1 -1
- package/dist/db/entities/SystemConfig.js +4 -0
- package/dist/db/entities/SystemConfig.js.map +1 -1
- package/dist/routes/index.js +8 -1
- package/dist/routes/index.js.map +1 -1
- package/dist/services/cimdClientService.js +2 -1
- package/dist/services/cimdClientService.js.map +1 -1
- package/dist/services/credentialBindingService.js +14 -8
- package/dist/services/credentialBindingService.js.map +1 -1
- package/dist/services/mcpOAuthProvider.js +40 -24
- package/dist/services/mcpOAuthProvider.js.map +1 -1
- package/dist/services/mcpService.js +120 -50
- package/dist/services/mcpService.js.map +1 -1
- package/dist/services/oauthService.js +3 -1
- package/dist/services/oauthService.js.map +1 -1
- package/dist/services/personalOAuthService.js +235 -0
- package/dist/services/personalOAuthService.js.map +1 -0
- package/dist/services/principalRuntimeService.js +67 -45
- package/dist/services/principalRuntimeService.js.map +1 -1
- package/dist/services/serverConfigPresenter.js +11 -1
- package/dist/services/serverConfigPresenter.js.map +1 -1
- package/dist/services/sseService.js +10 -30
- package/dist/services/sseService.js.map +1 -1
- package/dist/services/upstreamOAuthDisconnectService.js +2 -2
- package/dist/services/upstreamOAuthDisconnectService.js.map +1 -1
- package/dist/utils/credentialTemplate.js +37 -6
- package/dist/utils/credentialTemplate.js.map +1 -1
- package/dist/utils/migration.js +1 -0
- package/dist/utils/migration.js.map +1 -1
- package/dist/utils/protectedResource.js +44 -0
- package/dist/utils/protectedResource.js.map +1 -0
- package/dist/utils/serverConfigPersistence.js +13 -0
- package/dist/utils/serverConfigPersistence.js.map +1 -1
- package/dist/utils/ssrf.js +47 -12
- package/dist/utils/ssrf.js.map +1 -1
- package/dist/utils/upstreamError.js +8 -1
- package/dist/utils/upstreamError.js.map +1 -1
- package/frontend/dist/assets/{ActivityPage-DFLMe7o2.js → ActivityPage-Cmpa6PNN.js} +2 -2
- package/frontend/dist/assets/{ActivityPage-DFLMe7o2.js.map → ActivityPage-Cmpa6PNN.js.map} +1 -1
- package/frontend/dist/assets/{CopyClientConfigDialog-DukyyXVp.js → CopyClientConfigDialog-CAqbh1Uh.js} +3 -3
- package/frontend/dist/assets/{CopyClientConfigDialog-DukyyXVp.js.map → CopyClientConfigDialog-CAqbh1Uh.js.map} +1 -1
- package/frontend/dist/assets/{CredentialsPage-BpBCEmEE.js → CredentialsPage-Cwr2rAVa.js} +2 -2
- package/frontend/dist/assets/{CredentialsPage-BpBCEmEE.js.map → CredentialsPage-Cwr2rAVa.js.map} +1 -1
- package/frontend/dist/assets/{Dashboard-DjZkbAPt.js → Dashboard-7XIOjzwF.js} +2 -2
- package/frontend/dist/assets/{Dashboard-DjZkbAPt.js.map → Dashboard-7XIOjzwF.js.map} +1 -1
- package/frontend/dist/assets/EndpointCopy-CZVjSaZJ.js +2 -0
- package/frontend/dist/assets/{EndpointCopy-Bv-_FPmR.js.map → EndpointCopy-CZVjSaZJ.js.map} +1 -1
- package/frontend/dist/assets/{GroupsPage-B8BWRIWv.js → GroupsPage-BpHUw2qs.js} +3 -3
- package/frontend/dist/assets/{GroupsPage-B8BWRIWv.js.map → GroupsPage-BpHUw2qs.js.map} +1 -1
- package/frontend/dist/assets/{LoginPage-OH73zcbT.js → LoginPage-ClSBXFLy.js} +2 -2
- package/frontend/dist/assets/{LoginPage-OH73zcbT.js.map → LoginPage-ClSBXFLy.js.map} +1 -1
- package/frontend/dist/assets/{LogsPage-B6P_uQvL.js → LogsPage-CVuaA5oX.js} +2 -2
- package/frontend/dist/assets/{LogsPage-B6P_uQvL.js.map → LogsPage-CVuaA5oX.js.map} +1 -1
- package/frontend/dist/assets/{MarketPage-CKjkgZpe.js → MarketPage-BveXMx84.js} +2 -2
- package/frontend/dist/assets/{MarketPage-CKjkgZpe.js.map → MarketPage-BveXMx84.js.map} +1 -1
- package/frontend/dist/assets/{OAuthConsentPage-DjvtbLnm.js → OAuthConsentPage-B7kOLjXw.js} +2 -2
- package/frontend/dist/assets/{OAuthConsentPage-DjvtbLnm.js.map → OAuthConsentPage-B7kOLjXw.js.map} +1 -1
- package/frontend/dist/assets/{PromptsPage-CCMb9OH2.js → PromptsPage-DFH6bLuP.js} +2 -2
- package/frontend/dist/assets/{PromptsPage-CCMb9OH2.js.map → PromptsPage-DFH6bLuP.js.map} +1 -1
- package/frontend/dist/assets/{ResourcesPage-BERaKn0d.js → ResourcesPage-Dsjlzp3t.js} +2 -2
- package/frontend/dist/assets/{ResourcesPage-BERaKn0d.js.map → ResourcesPage-Dsjlzp3t.js.map} +1 -1
- package/frontend/dist/assets/ServersPage-BXEDxyLa.js +39 -0
- package/frontend/dist/assets/ServersPage-BXEDxyLa.js.map +1 -0
- package/frontend/dist/assets/SettingsPage-B1zU66rQ.js +14 -0
- package/frontend/dist/assets/SettingsPage-B1zU66rQ.js.map +1 -0
- package/frontend/dist/assets/{StatusDot-IWYCjv_t.js → StatusDot-Cp0G2KUZ.js} +2 -2
- package/frontend/dist/assets/{StatusDot-IWYCjv_t.js.map → StatusDot-Cp0G2KUZ.js.map} +1 -1
- package/frontend/dist/assets/{ToggleGroup-B_NGGU8p.js → ToggleGroup-BoQ324GW.js} +2 -2
- package/frontend/dist/assets/{ToggleGroup-B_NGGU8p.js.map → ToggleGroup-BoQ324GW.js.map} +1 -1
- package/frontend/dist/assets/{UsersPage-D9QdH-ma.js → UsersPage-B27T44p9.js} +2 -2
- package/frontend/dist/assets/{UsersPage-D9QdH-ma.js.map → UsersPage-B27T44p9.js.map} +1 -1
- package/frontend/dist/assets/{clipboard-CFeeSZ4D.js → clipboard-ChKqfVYn.js} +2 -2
- package/frontend/dist/assets/{clipboard-CFeeSZ4D.js.map → clipboard-ChKqfVYn.js.map} +1 -1
- package/frontend/dist/assets/{icons-vendor-CEwDZ1lN.js → icons-vendor-CkcU1LMA.js} +2 -2
- package/frontend/dist/assets/{icons-vendor-CEwDZ1lN.js.map → icons-vendor-CkcU1LMA.js.map} +1 -1
- package/frontend/dist/assets/index-dQlYMTl9.js +3 -0
- package/frontend/dist/assets/index-dQlYMTl9.js.map +1 -0
- package/frontend/dist/assets/{resourceService-BoJqsssR.js → resourceService-CmNa5REv.js} +2 -2
- package/frontend/dist/assets/{resourceService-BoJqsssR.js.map → resourceService-CmNa5REv.js.map} +1 -1
- package/frontend/dist/assets/{useCostData-Dld-MisV.js → useCostData-BrV9Dqf7.js} +2 -2
- package/frontend/dist/assets/{useCostData-Dld-MisV.js.map → useCostData-BrV9Dqf7.js.map} +1 -1
- package/frontend/dist/assets/variableDetection-sA1AjKyF.js +16 -0
- package/frontend/dist/assets/variableDetection-sA1AjKyF.js.map +1 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/EndpointCopy-Bv-_FPmR.js +0 -2
- package/frontend/dist/assets/ServersPage-BNsFiZez.js +0 -39
- package/frontend/dist/assets/ServersPage-BNsFiZez.js.map +0 -1
- package/frontend/dist/assets/SettingsPage-PiL7o9b9.js +0 -12
- package/frontend/dist/assets/SettingsPage-PiL7o9b9.js.map +0 -1
- package/frontend/dist/assets/index-RTKBtgGw.js +0 -3
- package/frontend/dist/assets/index-RTKBtgGw.js.map +0 -1
- package/frontend/dist/assets/variableDetection-C1abuc-p.js +0 -16
- package/frontend/dist/assets/variableDetection-C1abuc-p.js.map +0 -1
|
@@ -1,15 +1,17 @@
|
|
|
1
|
+
import { hasPersonalOAuth, PersonalOAuthContext, personalOAuthContexts, personalAuthorizationRequired, } from './personalOAuthService.js';
|
|
1
2
|
import { preserveListFreshness, combineListFreshness, remainingListTtl, } from '../utils/listFreshness.js';
|
|
2
3
|
import { expandServerConfig, getServerEnvironment } from './serverConfigEnvironment.js';
|
|
3
4
|
import { getMcpRequestGroup } from '../utils/mcpRequestGroup.js';
|
|
4
5
|
import { LegacyMcpClient } from '../clients/legacyMcpClient.js';
|
|
5
6
|
import { LEGACY_PROTOCOL_VERSIONS } from '../utils/mcpProtocol.js';
|
|
6
7
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
7
9
|
import { canAccessGroupRoute } from '../utils/groupAccess.js';
|
|
8
10
|
import { canServeToolRequests } from '../utils/serverAvailability.js';
|
|
9
11
|
import { PrincipalRuntimeService } from './principalRuntimeService.js';
|
|
10
12
|
import { UserContextService } from './userContextService.js';
|
|
11
13
|
import { deleteCredentialBindings, missingCredentialError } from './credentialBindingService.js';
|
|
12
|
-
import { CredentialBindingError, hasCredentialTemplate, validateCredentialTemplate, } from '../utils/credentialTemplate.js';
|
|
14
|
+
import { CredentialBindingError, hasCredentialTemplate, hasPersonalAuthentication, resolveCredentialArguments, validateCredentialTemplate, } from '../utils/credentialTemplate.js';
|
|
13
15
|
import os from 'os';
|
|
14
16
|
import path from 'path';
|
|
15
17
|
import fs from 'fs';
|
|
@@ -171,13 +173,13 @@ const sanitizeArgs = (args) => {
|
|
|
171
173
|
return args.map((arg) => {
|
|
172
174
|
// Remove shell metacharacters that could be used for injection
|
|
173
175
|
// Allow common flags (-f, --flag, -vvv) and paths
|
|
174
|
-
if (/^[a-zA-Z0-9._/\\:@=+,-]+$/.test(arg)) {
|
|
176
|
+
if (/^[a-zA-Z0-9._/\\:@=+,-]+$/.test(arg.replace(/\{\{args\.[A-Za-z_][A-Za-z0-9_]*\}\}/g, 'slot'))) {
|
|
175
177
|
return arg;
|
|
176
178
|
}
|
|
177
179
|
// For arguments with special characters, log a warning
|
|
178
180
|
// Also block redirection operators (>, <, >>) and newlines
|
|
179
181
|
logger.warn(`[proxychains] Potentially unsafe argument blocked: ${arg}`);
|
|
180
|
-
return arg.replace(
|
|
182
|
+
return arg.replace(/\{\{args\.[A-Za-z_][A-Za-z0-9_]*\}\}|[;&|`$(){}[\]!><\n\r]/g, (match) => match.startsWith('{{args.') ? match : '');
|
|
181
183
|
});
|
|
182
184
|
};
|
|
183
185
|
/**
|
|
@@ -721,16 +723,28 @@ const removeClientState = (id) => {
|
|
|
721
723
|
for (const client of state.cookieClients)
|
|
722
724
|
client.clearSessionCookies(id);
|
|
723
725
|
};
|
|
724
|
-
const acquireClientState = (serverInfo) => {
|
|
726
|
+
const acquireClientState = (serverInfo, openAiSession) => {
|
|
725
727
|
const contextService = RequestContextService.getInstance();
|
|
726
728
|
const scope = contextService.getRequestContext()?.clientStateScope;
|
|
727
|
-
const
|
|
728
|
-
if (!scope
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
729
|
+
const explicitHandle = contextService.getHeader('x-mcphub-state-id');
|
|
730
|
+
if (!scope) {
|
|
731
|
+
throw new Error('Stateful tools on stateless MCP require authenticated bearer access and an application state handle');
|
|
732
|
+
}
|
|
733
|
+
let handle;
|
|
734
|
+
if (explicitHandle !== undefined) {
|
|
735
|
+
if (typeof explicitHandle !== 'string' ||
|
|
736
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(explicitHandle)) {
|
|
737
|
+
throw new Error('X-MCPHub-State-Id must be a UUID v4');
|
|
738
|
+
}
|
|
739
|
+
handle = explicitHandle.toLowerCase();
|
|
740
|
+
}
|
|
741
|
+
else if (typeof openAiSession === 'string' && openAiSession.trim()) {
|
|
742
|
+
handle = `openai:${createHash('sha256').update(openAiSession).digest('hex')}`;
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
throw new Error('Stateful tools on stateless MCP require X-MCPHub-State-Id (UUID v4) or _meta["openai/session"]');
|
|
732
746
|
}
|
|
733
|
-
const id = `client-state:${scope}:${handle
|
|
747
|
+
const id = `client-state:${scope}:${handle}`;
|
|
734
748
|
let state = clientStates.get(id);
|
|
735
749
|
if (state && state.active === 0 && Date.now() - state.lastUsed >= CLIENT_STATE_IDLE_MS) {
|
|
736
750
|
removeClientState(id);
|
|
@@ -895,7 +909,7 @@ export const summarizeServerConnections = (infos) => {
|
|
|
895
909
|
};
|
|
896
910
|
export const getServerConnectionStats = () => {
|
|
897
911
|
// Personal definitions have no shared connection whose readiness can be checked.
|
|
898
|
-
return summarizeServerConnections(serverInfos.filter((info) => !
|
|
912
|
+
return summarizeServerConnections(serverInfos.filter((info) => !hasPersonalAuthentication(info.config)));
|
|
899
913
|
};
|
|
900
914
|
// Returns true if all enabled servers are connected
|
|
901
915
|
export const connected = () => {
|
|
@@ -1194,7 +1208,7 @@ export const createTransportFromConfig = async (name, conf) => {
|
|
|
1194
1208
|
if (conf.type === 'streamable-http') {
|
|
1195
1209
|
const options = {};
|
|
1196
1210
|
let headers = conf.headers
|
|
1197
|
-
?
|
|
1211
|
+
? hasPersonalAuthentication(conf)
|
|
1198
1212
|
? conf.headers
|
|
1199
1213
|
: replaceEnvVars(conf.headers, env)
|
|
1200
1214
|
: {};
|
|
@@ -1220,14 +1234,15 @@ export const createTransportFromConfig = async (name, conf) => {
|
|
|
1220
1234
|
headers,
|
|
1221
1235
|
};
|
|
1222
1236
|
}
|
|
1223
|
-
options.fetch =
|
|
1237
|
+
options.fetch =
|
|
1238
|
+
personalOAuthContexts.get(conf)?.wrapFetch(requestAwareFetch) || requestAwareFetch;
|
|
1224
1239
|
transport = new StreamableHTTPClientTransport(new URL(conf.url || ''), options);
|
|
1225
1240
|
}
|
|
1226
1241
|
else if (conf.url) {
|
|
1227
1242
|
// SSE transport
|
|
1228
1243
|
const options = {};
|
|
1229
1244
|
let headers = conf.headers
|
|
1230
|
-
?
|
|
1245
|
+
? hasPersonalAuthentication(conf)
|
|
1231
1246
|
? conf.headers
|
|
1232
1247
|
: replaceEnvVars(conf.headers, env)
|
|
1233
1248
|
: {};
|
|
@@ -1246,7 +1261,7 @@ export const createTransportFromConfig = async (name, conf) => {
|
|
|
1246
1261
|
if (Object.keys(headers).length > 0) {
|
|
1247
1262
|
options.eventSourceInit = {
|
|
1248
1263
|
headers,
|
|
1249
|
-
fetch: requestAwareFetch,
|
|
1264
|
+
fetch: personalOAuthContexts.get(conf)?.wrapFetch(requestAwareFetch) || requestAwareFetch,
|
|
1250
1265
|
};
|
|
1251
1266
|
options.requestInit = {
|
|
1252
1267
|
headers,
|
|
@@ -1254,10 +1269,11 @@ export const createTransportFromConfig = async (name, conf) => {
|
|
|
1254
1269
|
}
|
|
1255
1270
|
else {
|
|
1256
1271
|
options.eventSourceInit = {
|
|
1257
|
-
fetch: requestAwareFetch,
|
|
1272
|
+
fetch: personalOAuthContexts.get(conf)?.wrapFetch(requestAwareFetch) || requestAwareFetch,
|
|
1258
1273
|
};
|
|
1259
1274
|
}
|
|
1260
|
-
options.fetch =
|
|
1275
|
+
options.fetch =
|
|
1276
|
+
personalOAuthContexts.get(conf)?.wrapFetch(requestAwareFetch) || requestAwareFetch;
|
|
1261
1277
|
transport = new SSEClientTransport(new URL(conf.url), options);
|
|
1262
1278
|
}
|
|
1263
1279
|
else if (conf.command) {
|
|
@@ -1292,12 +1308,13 @@ export const createTransportFromConfig = async (name, conf) => {
|
|
|
1292
1308
|
transport = new StdioClientTransport({
|
|
1293
1309
|
cwd: process.cwd(),
|
|
1294
1310
|
command: finalCommand,
|
|
1295
|
-
|
|
1311
|
+
// Substitute after expansion and proxy argument cleaning so secrets stay literal.
|
|
1312
|
+
args: resolveCredentialArguments(conf, finalArgs),
|
|
1296
1313
|
env: env,
|
|
1297
1314
|
stderr: 'pipe',
|
|
1298
1315
|
maxBufferSize: validateMaxBufferSize(conf.options?.maxBufferSize),
|
|
1299
1316
|
});
|
|
1300
|
-
if (transport.stderr &&
|
|
1317
|
+
if (transport.stderr && hasPersonalAuthentication(conf)) {
|
|
1301
1318
|
transport.stderr.on('data', () => undefined); // Personal upstream diagnostics can contain raw credentials.
|
|
1302
1319
|
}
|
|
1303
1320
|
else if (transport.stderr) {
|
|
@@ -1324,7 +1341,7 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
|
|
|
1324
1341
|
if (!client) {
|
|
1325
1342
|
throw new Error(`Client not found for server: ${serverInfo.name}`);
|
|
1326
1343
|
}
|
|
1327
|
-
if (
|
|
1344
|
+
if (hasPersonalAuthentication(serverInfo.config)) {
|
|
1328
1345
|
return client.callTool(toolParams, options || {});
|
|
1329
1346
|
}
|
|
1330
1347
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
@@ -1501,7 +1518,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1501
1518
|
});
|
|
1502
1519
|
continue;
|
|
1503
1520
|
}
|
|
1504
|
-
if (
|
|
1521
|
+
if (hasPersonalAuthentication(expandedConf)) {
|
|
1505
1522
|
validateCredentialTemplate(expandedConf);
|
|
1506
1523
|
nextServerInfos.push({
|
|
1507
1524
|
name,
|
|
@@ -2010,15 +2027,26 @@ export const getServersInfo = async (page, limit, user) => {
|
|
|
2010
2027
|
const principal = (user ||
|
|
2011
2028
|
UserContextService.getInstance().getCurrentUser());
|
|
2012
2029
|
const presentedInfos = await Promise.all(filterServerInfos.map(async (info) => {
|
|
2013
|
-
if (!
|
|
2030
|
+
if (!hasPersonalAuthentication(info.config) ||
|
|
2014
2031
|
info.enabled === false ||
|
|
2015
2032
|
!principal?.username ||
|
|
2016
2033
|
principal.credentialEligible === false)
|
|
2017
2034
|
return info;
|
|
2035
|
+
let ownOAuth;
|
|
2018
2036
|
try {
|
|
2037
|
+
const personal = hasPersonalOAuth(info.config)
|
|
2038
|
+
? await PersonalOAuthContext.load(info.name, principal.username, (await getServerDao().findById(info.name)))
|
|
2039
|
+
: undefined;
|
|
2040
|
+
ownOAuth = personal
|
|
2041
|
+
? {
|
|
2042
|
+
identityMode: 'personal',
|
|
2043
|
+
connected: Boolean(personal.config.oauth.accessToken),
|
|
2044
|
+
authorizationUrl: personal.authorizationUrl,
|
|
2045
|
+
}
|
|
2046
|
+
: undefined;
|
|
2019
2047
|
const lease = await principalRuntimes.acquire(info.name, principal);
|
|
2020
2048
|
try {
|
|
2021
|
-
return { ...lease.info, config: info.config };
|
|
2049
|
+
return { ...lease.info, config: info.config, ...(ownOAuth ? { oauth: ownOAuth } : {}) };
|
|
2022
2050
|
}
|
|
2023
2051
|
finally {
|
|
2024
2052
|
lease.release();
|
|
@@ -2027,6 +2055,7 @@ export const getServersInfo = async (page, limit, user) => {
|
|
|
2027
2055
|
catch (error) {
|
|
2028
2056
|
return {
|
|
2029
2057
|
...info,
|
|
2058
|
+
...(ownOAuth ? { status: 'oauth_required', oauth: ownOAuth } : {}),
|
|
2030
2059
|
error: error instanceof CredentialBindingError || error instanceof UpstreamRequestError
|
|
2031
2060
|
? error.message
|
|
2032
2061
|
: 'Unable to prepare upstream server',
|
|
@@ -2089,6 +2118,8 @@ export const getServersInfo = async (page, limit, user) => {
|
|
|
2089
2118
|
? {
|
|
2090
2119
|
...(oauth
|
|
2091
2120
|
? {
|
|
2121
|
+
...(oauth.identityMode ? { identityMode: oauth.identityMode } : {}),
|
|
2122
|
+
...(oauth.connected ? { connected: true } : {}),
|
|
2092
2123
|
authorizationUrl: oauth.authorizationUrl,
|
|
2093
2124
|
state: oauth.state,
|
|
2094
2125
|
...(oauth.clientIdConfigured ? { clientIdConfigured: true } : {}),
|
|
@@ -2101,9 +2132,12 @@ export const getServersInfo = async (page, limit, user) => {
|
|
|
2101
2132
|
config: resolvedType || serverConfig?.description || serverConfig?.command
|
|
2102
2133
|
? {
|
|
2103
2134
|
...(resolvedType ? { type: resolvedType } : {}),
|
|
2104
|
-
...(
|
|
2135
|
+
...(hasPersonalAuthentication(serverConfig)
|
|
2105
2136
|
? { credentialTemplate: validateCredentialTemplate(serverConfig) }
|
|
2106
2137
|
: {}),
|
|
2138
|
+
...(hasPersonalOAuth(serverConfig)
|
|
2139
|
+
? { oauth: { identityMode: 'personal' } }
|
|
2140
|
+
: {}),
|
|
2107
2141
|
...(serverConfig?.description ? { description: serverConfig.description } : {}),
|
|
2108
2142
|
// Expose command so the frontend can determine if reinstall is
|
|
2109
2143
|
// supported (npx/uvx only). This is not a secret — it's the
|
|
@@ -2493,7 +2527,7 @@ const scheduleIdleShutdown = (serverInfo) => {
|
|
|
2493
2527
|
clearIdleShutdown(serverInfo);
|
|
2494
2528
|
if (!serverInfo.config?.startOnDemand ||
|
|
2495
2529
|
!isStdioServer(serverInfo.config) ||
|
|
2496
|
-
|
|
2530
|
+
hasPersonalAuthentication(serverInfo.config) ||
|
|
2497
2531
|
serverInfo.status !== 'connected' ||
|
|
2498
2532
|
!serverInfo.client ||
|
|
2499
2533
|
serverInfo.activeToolCalls) {
|
|
@@ -2509,7 +2543,7 @@ const scheduleIdleShutdown = (serverInfo) => {
|
|
|
2509
2543
|
const beginOnDemandToolCall = (serverInfo) => {
|
|
2510
2544
|
if (!serverInfo.config?.startOnDemand ||
|
|
2511
2545
|
!isStdioServer(serverInfo.config) ||
|
|
2512
|
-
|
|
2546
|
+
hasPersonalAuthentication(serverInfo.config)) {
|
|
2513
2547
|
return undefined;
|
|
2514
2548
|
}
|
|
2515
2549
|
clearIdleShutdown(serverInfo);
|
|
@@ -2657,7 +2691,7 @@ const isStdioServer = (conf) => !!conf && (conf.type === 'stdio' || (!conf.type
|
|
|
2657
2691
|
*/
|
|
2658
2692
|
const primeOnDemandServers = (gate) => {
|
|
2659
2693
|
const targets = serverInfos.filter((si) => si.config?.startOnDemand === true &&
|
|
2660
|
-
!
|
|
2694
|
+
!hasPersonalAuthentication(si.config) &&
|
|
2661
2695
|
isStdioServer(si.config) &&
|
|
2662
2696
|
si.tools.length === 0 &&
|
|
2663
2697
|
si.enabled !== false);
|
|
@@ -2865,12 +2899,17 @@ const getMcpAppsRouteContext = async (sessionId, group) => {
|
|
|
2865
2899
|
const eligible = (pinned?.pinnedServerInfos ?? []).filter(canServeToolRequests);
|
|
2866
2900
|
return eligible.length > 0 ? { enabled: true, serverInfos: eligible } : { enabled: false };
|
|
2867
2901
|
}
|
|
2868
|
-
const { filteredServerInfos } = await getFilteredServerInfosForGroup(group);
|
|
2902
|
+
const { filteredServerInfos, serverConfigsByName } = await getFilteredServerInfosForGroup(group);
|
|
2869
2903
|
const connectedServerInfos = filteredServerInfos.filter((serverInfo) => serverInfo.status === 'connected' && !!serverInfo.client);
|
|
2870
2904
|
if (connectedServerInfos.length === 0) {
|
|
2871
2905
|
return { enabled: false };
|
|
2872
2906
|
}
|
|
2873
|
-
|
|
2907
|
+
// Connection availability must not turn an aggregate route into a raw-name
|
|
2908
|
+
// route: OpenAPI uses openApiClient, and tool calls acquire only the target
|
|
2909
|
+
// personal runtime. Match resolveToolInGroup's configured-route ambiguity check.
|
|
2910
|
+
if (connectedServerInfos.length === 1 &&
|
|
2911
|
+
filteredServerInfos.length === 1 &&
|
|
2912
|
+
serverConfigsByName.size <= 1) {
|
|
2874
2913
|
return {
|
|
2875
2914
|
enabled: true,
|
|
2876
2915
|
serverInfo: connectedServerInfos[0],
|
|
@@ -3010,7 +3049,7 @@ const projectToolForDownstream = (serverName, tool, appsRouteContext, serverConf
|
|
|
3010
3049
|
return undefined;
|
|
3011
3050
|
}
|
|
3012
3051
|
const projectedTool = appsRouteContext.enabled ? tool : stripMcpAppsMetadata(tool);
|
|
3013
|
-
// Single
|
|
3052
|
+
// Single configured server: keep the existing raw (unqualified) name so
|
|
3014
3053
|
// widgets that call back with the bare tool name continue to work.
|
|
3015
3054
|
if (appsRouteContext.serverInfo) {
|
|
3016
3055
|
return {
|
|
@@ -3018,7 +3057,7 @@ const projectToolForDownstream = (serverName, tool, appsRouteContext, serverConf
|
|
|
3018
3057
|
name: normalizeToolNameForServer(serverName, projectedTool.name),
|
|
3019
3058
|
};
|
|
3020
3059
|
}
|
|
3021
|
-
//
|
|
3060
|
+
// Aggregate routes (Apps-enabled) or Apps disabled: qualify the
|
|
3022
3061
|
// name so it can still be routed to the right server unambiguously.
|
|
3023
3062
|
return {
|
|
3024
3063
|
...projectedTool,
|
|
@@ -3162,7 +3201,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3162
3201
|
const getToolStateId = (info, legacyId) => {
|
|
3163
3202
|
if (!requestContextService.getRequestContext()?.stateless)
|
|
3164
3203
|
return legacyId;
|
|
3165
|
-
clientStateLease ?? (clientStateLease = acquireClientState(info));
|
|
3204
|
+
clientStateLease ?? (clientStateLease = acquireClientState(info, request.params?._meta?.['openai/session']));
|
|
3166
3205
|
return clientStateLease.id;
|
|
3167
3206
|
};
|
|
3168
3207
|
const reserveHostedIfNeeded = async (serverName, toolName) => {
|
|
@@ -3320,7 +3359,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3320
3359
|
keyId,
|
|
3321
3360
|
keyName,
|
|
3322
3361
|
sourceIp,
|
|
3323
|
-
hasCredentialTemplate:
|
|
3362
|
+
hasCredentialTemplate: hasPersonalAuthentication(targetServerInfo.config),
|
|
3324
3363
|
});
|
|
3325
3364
|
return await maybeCompressToolResult({
|
|
3326
3365
|
content: [
|
|
@@ -3339,7 +3378,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3339
3378
|
// For servers with perSessionClient: true, use a per-session dedicated client
|
|
3340
3379
|
let isolatedCtx;
|
|
3341
3380
|
if (targetServerInfo.config?.perSessionClient &&
|
|
3342
|
-
!
|
|
3381
|
+
!hasPersonalAuthentication(targetServerInfo.config)) {
|
|
3343
3382
|
const stateId = getToolStateId(targetServerInfo, sessionId);
|
|
3344
3383
|
if (!stateId) {
|
|
3345
3384
|
throw new Error(`Server '${targetServerInfo.name}' requires per-session client isolation and is not yet available on stateless MCP requests`);
|
|
@@ -3392,7 +3431,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3392
3431
|
keyId,
|
|
3393
3432
|
keyName,
|
|
3394
3433
|
sourceIp,
|
|
3395
|
-
hasCredentialTemplate:
|
|
3434
|
+
hasCredentialTemplate: hasPersonalAuthentication(targetServerInfo.config),
|
|
3396
3435
|
errorMessage: result.isError ? 'Tool returned error response' : undefined,
|
|
3397
3436
|
});
|
|
3398
3437
|
return await maybeCompressToolResult(result, {
|
|
@@ -3499,7 +3538,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3499
3538
|
keyId,
|
|
3500
3539
|
keyName,
|
|
3501
3540
|
sourceIp,
|
|
3502
|
-
hasCredentialTemplate:
|
|
3541
|
+
hasCredentialTemplate: hasPersonalAuthentication(serverInfo.config),
|
|
3503
3542
|
});
|
|
3504
3543
|
return await maybeCompressToolResult({
|
|
3505
3544
|
content: [
|
|
@@ -3517,7 +3556,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3517
3556
|
// Handle MCP servers
|
|
3518
3557
|
// For servers with perSessionClient: true, use a per-session dedicated client
|
|
3519
3558
|
let isolatedCtx;
|
|
3520
|
-
if (serverInfo.config?.perSessionClient && !
|
|
3559
|
+
if (serverInfo.config?.perSessionClient && !hasPersonalAuthentication(serverInfo.config)) {
|
|
3521
3560
|
const stateId = getToolStateId(serverInfo, sessionId);
|
|
3522
3561
|
if (!stateId) {
|
|
3523
3562
|
throw new Error(`Server '${serverInfo.name}' requires per-session client isolation and is not yet available on stateless MCP requests`);
|
|
@@ -3555,7 +3594,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3555
3594
|
keyId,
|
|
3556
3595
|
keyName,
|
|
3557
3596
|
sourceIp,
|
|
3558
|
-
hasCredentialTemplate:
|
|
3597
|
+
hasCredentialTemplate: hasPersonalAuthentication(serverInfo.config),
|
|
3559
3598
|
errorMessage: result.isError ? 'Tool returned error response' : undefined,
|
|
3560
3599
|
});
|
|
3561
3600
|
return await maybeCompressToolResult(result, {
|
|
@@ -3594,7 +3633,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3594
3633
|
keyId,
|
|
3595
3634
|
keyName,
|
|
3596
3635
|
sourceIp,
|
|
3597
|
-
hasCredentialTemplate:
|
|
3636
|
+
hasCredentialTemplate: hasPersonalAuthentication(serverInfo?.config),
|
|
3598
3637
|
// The reason is an internal diagnostic; the caller-facing message stays
|
|
3599
3638
|
// identical for hidden and nonexistent targets.
|
|
3600
3639
|
errorMessage: unavailable
|
|
@@ -3867,9 +3906,19 @@ const handleReadResourceRequestImpl = async (request, extra) => {
|
|
|
3867
3906
|
? { cacheMode: 'bypass' }
|
|
3868
3907
|
: undefined;
|
|
3869
3908
|
if (server?.client) {
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3909
|
+
// A widget URI the upstream also lists in resources/list is read here, not in
|
|
3910
|
+
// the fallback loop below, so it needs the same idle guard: otherwise an
|
|
3911
|
+
// idle timer armed by an earlier read can close an on-demand upstream while
|
|
3912
|
+
// this read is still in flight.
|
|
3913
|
+
const releaseOnDemandRead = beginOnDemandToolCall(server);
|
|
3914
|
+
try {
|
|
3915
|
+
result = readOptions
|
|
3916
|
+
? await server.client.readResource({ uri }, readOptions)
|
|
3917
|
+
: await server.client.readResource({ uri });
|
|
3918
|
+
}
|
|
3919
|
+
finally {
|
|
3920
|
+
releaseOnDemandRead?.();
|
|
3921
|
+
}
|
|
3873
3922
|
if (!result || !Array.isArray(result.contents)) {
|
|
3874
3923
|
throw new Error(`Failed to read resource: ${uri}`);
|
|
3875
3924
|
}
|
|
@@ -3885,10 +3934,23 @@ const handleReadResourceRequestImpl = async (request, extra) => {
|
|
|
3885
3934
|
? [appsRouteContext.serverInfo]
|
|
3886
3935
|
: (appsRouteContext.serverInfos ?? []);
|
|
3887
3936
|
for (const candidate of candidates) {
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3937
|
+
// An on-demand candidate may be idle yet still advertise its cached widget
|
|
3938
|
+
// link, so a host can read the ui:// resource before any tool call has
|
|
3939
|
+
// woken it. Hold off its idle shutdown for every read, awake or not, so an
|
|
3940
|
+
// armed idle timer or an overlapping read cannot close the server mid-read,
|
|
3941
|
+
// then wake it the same way the call path does if it is not running.
|
|
3942
|
+
let releaseOnDemandRead;
|
|
3891
3943
|
try {
|
|
3944
|
+
if (candidate.config?.startOnDemand && candidate.enabled !== false) {
|
|
3945
|
+
releaseOnDemandRead = beginOnDemandToolCall(candidate);
|
|
3946
|
+
if (!candidate.client) {
|
|
3947
|
+
await ensureServerReady(candidate);
|
|
3948
|
+
}
|
|
3949
|
+
candidate.lastUsedAt = Date.now();
|
|
3950
|
+
}
|
|
3951
|
+
if (!candidate.client) {
|
|
3952
|
+
continue;
|
|
3953
|
+
}
|
|
3892
3954
|
const candidateResult = readOptions
|
|
3893
3955
|
? await candidate.client.readResource({ uri }, readOptions)
|
|
3894
3956
|
: await candidate.client.readResource({ uri });
|
|
@@ -3898,7 +3960,10 @@ const handleReadResourceRequestImpl = async (request, extra) => {
|
|
|
3898
3960
|
}
|
|
3899
3961
|
}
|
|
3900
3962
|
catch {
|
|
3901
|
-
// This candidate doesn't own the resource; try the next one.
|
|
3963
|
+
// This candidate doesn't own the resource (or failed to wake); try the next one.
|
|
3964
|
+
}
|
|
3965
|
+
finally {
|
|
3966
|
+
releaseOnDemandRead?.();
|
|
3902
3967
|
}
|
|
3903
3968
|
}
|
|
3904
3969
|
if (!result) {
|
|
@@ -4029,6 +4094,9 @@ const createPrincipalRuntime = async (name, resolvedConfig) => {
|
|
|
4029
4094
|
}
|
|
4030
4095
|
catch (error) {
|
|
4031
4096
|
closeServerRuntime(info);
|
|
4097
|
+
if (hasPersonalOAuth(resolvedConfig) &&
|
|
4098
|
+
personalOAuthContexts.get(resolvedConfig)?.config.oauth.pendingAuthorization)
|
|
4099
|
+
throw personalAuthorizationRequired(name);
|
|
4032
4100
|
const failure = createUpstreamRequestError(name, 'connect', error, resolvedConfig);
|
|
4033
4101
|
logger.error(failure.message);
|
|
4034
4102
|
throw failure;
|
|
@@ -4058,7 +4126,7 @@ export const getServerToolsForPrincipal = async (serverName, principal) => {
|
|
|
4058
4126
|
}
|
|
4059
4127
|
};
|
|
4060
4128
|
const withPrincipalServers = (handler, operation) => async (request, extra) => {
|
|
4061
|
-
if (!serverInfos.some((info) =>
|
|
4129
|
+
if (!serverInfos.some((info) => hasPersonalAuthentication(info.config)))
|
|
4062
4130
|
return handler(request, extra);
|
|
4063
4131
|
// Built-in content does not require an upstream credential.
|
|
4064
4132
|
if (operation === 'prompt') {
|
|
@@ -4095,7 +4163,7 @@ const withPrincipalServers = (handler, operation) => async (request, extra) => {
|
|
|
4095
4163
|
}
|
|
4096
4164
|
// A call to a known target must not spawn or keep unrelated personal children alive.
|
|
4097
4165
|
const selected = target && (operation === 'tool' || operation === 'prompt') ? [target] : candidates;
|
|
4098
|
-
const templated = selected.filter((info) =>
|
|
4166
|
+
const templated = selected.filter((info) => hasPersonalAuthentication(info.config));
|
|
4099
4167
|
if (!templated.length)
|
|
4100
4168
|
return handler(request, extra);
|
|
4101
4169
|
const user = UserContextService.getInstance().getCurrentUser();
|
|
@@ -4108,7 +4176,9 @@ const withPrincipalServers = (handler, operation) => async (request, extra) => {
|
|
|
4108
4176
|
if (!user?.username ||
|
|
4109
4177
|
user.credentialEligible === false ||
|
|
4110
4178
|
RequestContextService.getInstance().getKeyKindContext() === 'system')
|
|
4111
|
-
throw
|
|
4179
|
+
throw hasPersonalOAuth(info.config)
|
|
4180
|
+
? personalAuthorizationRequired(info.name)
|
|
4181
|
+
: missingCredentialError(info.name);
|
|
4112
4182
|
const lease = await principalRuntimes.acquire(info.name, user);
|
|
4113
4183
|
releases.push(lease.release);
|
|
4114
4184
|
scoped.set(info.name, lease.info);
|
|
@@ -4137,7 +4207,7 @@ const withPrincipalServers = (handler, operation) => async (request, extra) => {
|
|
|
4137
4207
|
let resolvedRequest = request;
|
|
4138
4208
|
if (operation === 'tool' &&
|
|
4139
4209
|
extra?.server &&
|
|
4140
|
-
|
|
4210
|
+
hasPersonalAuthentication(scoped.get(extra.server)?.config)) {
|
|
4141
4211
|
const prefix = `${extra.server}${getNameSeparator()}`;
|
|
4142
4212
|
if (typeof requestedName === 'string' && !requestedName.startsWith(prefix)) {
|
|
4143
4213
|
resolvedRequest =
|