@samanhappy/mcphub 1.1.3 → 1.1.5

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.
Files changed (104) hide show
  1. package/README.fr.md +2 -0
  2. package/README.md +2 -0
  3. package/README.zh.md +2 -0
  4. package/dist/controllers/mcpbController.js +6 -2
  5. package/dist/controllers/mcpbController.js.map +1 -1
  6. package/dist/controllers/oauthCallbackController.js +11 -16
  7. package/dist/controllers/oauthCallbackController.js.map +1 -1
  8. package/dist/controllers/oauthServerController.js +10 -7
  9. package/dist/controllers/oauthServerController.js.map +1 -1
  10. package/dist/controllers/personalOAuthController.js +119 -0
  11. package/dist/controllers/personalOAuthController.js.map +1 -0
  12. package/dist/controllers/serverController.js +2 -2
  13. package/dist/controllers/serverController.js.map +1 -1
  14. package/dist/controllers/smartRoutingController.js +4 -4
  15. package/dist/controllers/smartRoutingController.js.map +1 -1
  16. package/dist/dao/CredentialBindingDao.js +8 -0
  17. package/dist/dao/CredentialBindingDao.js.map +1 -1
  18. package/dist/dao/CredentialBindingDaoDbImpl.js +14 -0
  19. package/dist/dao/CredentialBindingDaoDbImpl.js.map +1 -1
  20. package/dist/routes/index.js +8 -1
  21. package/dist/routes/index.js.map +1 -1
  22. package/dist/services/credentialBindingService.js +14 -8
  23. package/dist/services/credentialBindingService.js.map +1 -1
  24. package/dist/services/groupService.js +5 -1
  25. package/dist/services/groupService.js.map +1 -1
  26. package/dist/services/mcpOAuthProvider.js +40 -24
  27. package/dist/services/mcpOAuthProvider.js.map +1 -1
  28. package/dist/services/mcpService.js +66 -35
  29. package/dist/services/mcpService.js.map +1 -1
  30. package/dist/services/oauthService.js +3 -1
  31. package/dist/services/oauthService.js.map +1 -1
  32. package/dist/services/personalOAuthService.js +235 -0
  33. package/dist/services/personalOAuthService.js.map +1 -0
  34. package/dist/services/principalRuntimeService.js +67 -45
  35. package/dist/services/principalRuntimeService.js.map +1 -1
  36. package/dist/services/serverConfigPresenter.js +11 -1
  37. package/dist/services/serverConfigPresenter.js.map +1 -1
  38. package/dist/services/sseService.js +8 -28
  39. package/dist/services/sseService.js.map +1 -1
  40. package/dist/services/upstreamOAuthDisconnectService.js +2 -2
  41. package/dist/services/upstreamOAuthDisconnectService.js.map +1 -1
  42. package/dist/utils/credentialTemplate.js +37 -6
  43. package/dist/utils/credentialTemplate.js.map +1 -1
  44. package/dist/utils/protectedResource.js +44 -0
  45. package/dist/utils/protectedResource.js.map +1 -0
  46. package/dist/utils/serverConfigPersistence.js +13 -0
  47. package/dist/utils/serverConfigPersistence.js.map +1 -1
  48. package/dist/utils/upstreamError.js +8 -1
  49. package/dist/utils/upstreamError.js.map +1 -1
  50. package/frontend/dist/assets/{ActivityPage-Bs0YrAhj.js → ActivityPage-BLDzY9xr.js} +2 -2
  51. package/frontend/dist/assets/{ActivityPage-Bs0YrAhj.js.map → ActivityPage-BLDzY9xr.js.map} +1 -1
  52. package/frontend/dist/assets/{CopyClientConfigDialog-TcVaJHaV.js → CopyClientConfigDialog-gCW3MVjV.js} +3 -3
  53. package/frontend/dist/assets/{CopyClientConfigDialog-TcVaJHaV.js.map → CopyClientConfigDialog-gCW3MVjV.js.map} +1 -1
  54. package/frontend/dist/assets/{CredentialsPage-Cnd6MF-3.js → CredentialsPage-Dty6-gb9.js} +2 -2
  55. package/frontend/dist/assets/{CredentialsPage-Cnd6MF-3.js.map → CredentialsPage-Dty6-gb9.js.map} +1 -1
  56. package/frontend/dist/assets/{Dashboard--skdgzau.js → Dashboard-Os03VW_9.js} +2 -2
  57. package/frontend/dist/assets/{Dashboard--skdgzau.js.map → Dashboard-Os03VW_9.js.map} +1 -1
  58. package/frontend/dist/assets/EndpointCopy-ByVAj2YF.js +2 -0
  59. package/frontend/dist/assets/{EndpointCopy-Dr-iLvMr.js.map → EndpointCopy-ByVAj2YF.js.map} +1 -1
  60. package/frontend/dist/assets/{GroupsPage-CSIfAW7V.js → GroupsPage-BQ0if-JQ.js} +3 -3
  61. package/frontend/dist/assets/{GroupsPage-CSIfAW7V.js.map → GroupsPage-BQ0if-JQ.js.map} +1 -1
  62. package/frontend/dist/assets/{LoginPage-DwnZEV5T.js → LoginPage-D7HK95LF.js} +2 -2
  63. package/frontend/dist/assets/{LoginPage-DwnZEV5T.js.map → LoginPage-D7HK95LF.js.map} +1 -1
  64. package/frontend/dist/assets/{LogsPage-CTstXCQT.js → LogsPage-BcIdaRgU.js} +2 -2
  65. package/frontend/dist/assets/{LogsPage-CTstXCQT.js.map → LogsPage-BcIdaRgU.js.map} +1 -1
  66. package/frontend/dist/assets/{MarketPage-IbsIGvoI.js → MarketPage-C3mMsTyI.js} +2 -2
  67. package/frontend/dist/assets/{MarketPage-IbsIGvoI.js.map → MarketPage-C3mMsTyI.js.map} +1 -1
  68. package/frontend/dist/assets/{OAuthConsentPage-DawUB5r6.js → OAuthConsentPage-3mefonjY.js} +2 -2
  69. package/frontend/dist/assets/{OAuthConsentPage-DawUB5r6.js.map → OAuthConsentPage-3mefonjY.js.map} +1 -1
  70. package/frontend/dist/assets/{PromptsPage-Dim3CdKg.js → PromptsPage-Bdb2xr1e.js} +2 -2
  71. package/frontend/dist/assets/{PromptsPage-Dim3CdKg.js.map → PromptsPage-Bdb2xr1e.js.map} +1 -1
  72. package/frontend/dist/assets/{ResourcesPage-zcP5M9pv.js → ResourcesPage-CNLpuEQh.js} +2 -2
  73. package/frontend/dist/assets/{ResourcesPage-zcP5M9pv.js.map → ResourcesPage-CNLpuEQh.js.map} +1 -1
  74. package/frontend/dist/assets/ServersPage-CUbyxl2S.js +39 -0
  75. package/frontend/dist/assets/ServersPage-CUbyxl2S.js.map +1 -0
  76. package/frontend/dist/assets/{SettingsPage-CoAt9Wha.js → SettingsPage-O4pB7hCL.js} +2 -2
  77. package/frontend/dist/assets/{SettingsPage-CoAt9Wha.js.map → SettingsPage-O4pB7hCL.js.map} +1 -1
  78. package/frontend/dist/assets/{StatusDot-nh6JXzfm.js → StatusDot-CVsYfY8W.js} +2 -2
  79. package/frontend/dist/assets/{StatusDot-nh6JXzfm.js.map → StatusDot-CVsYfY8W.js.map} +1 -1
  80. package/frontend/dist/assets/{ToggleGroup-CeiSM3PL.js → ToggleGroup-DQNEO10y.js} +2 -2
  81. package/frontend/dist/assets/{ToggleGroup-CeiSM3PL.js.map → ToggleGroup-DQNEO10y.js.map} +1 -1
  82. package/frontend/dist/assets/{UsersPage-DLq5cgUz.js → UsersPage-DNPJIQt5.js} +2 -2
  83. package/frontend/dist/assets/{UsersPage-DLq5cgUz.js.map → UsersPage-DNPJIQt5.js.map} +1 -1
  84. package/frontend/dist/assets/{clipboard-CmkeMDp6.js → clipboard-CtMMKUq_.js} +2 -2
  85. package/frontend/dist/assets/{clipboard-CmkeMDp6.js.map → clipboard-CtMMKUq_.js.map} +1 -1
  86. package/frontend/dist/assets/{icons-vendor-CEwDZ1lN.js → icons-vendor-CkcU1LMA.js} +2 -2
  87. package/frontend/dist/assets/{icons-vendor-CEwDZ1lN.js.map → icons-vendor-CkcU1LMA.js.map} +1 -1
  88. package/frontend/dist/assets/index-D0MN2Xj2.js +3 -0
  89. package/frontend/dist/assets/index-D0MN2Xj2.js.map +1 -0
  90. package/frontend/dist/assets/{resourceService-Nk5qQbQ0.js → resourceService-BTcWcm4-.js} +2 -2
  91. package/frontend/dist/assets/{resourceService-Nk5qQbQ0.js.map → resourceService-BTcWcm4-.js.map} +1 -1
  92. package/frontend/dist/assets/{useCostData-BgXlKVml.js → useCostData-A1KGcEDJ.js} +2 -2
  93. package/frontend/dist/assets/{useCostData-BgXlKVml.js.map → useCostData-A1KGcEDJ.js.map} +1 -1
  94. package/frontend/dist/assets/variableDetection-BegSXlVi.js +16 -0
  95. package/frontend/dist/assets/variableDetection-BegSXlVi.js.map +1 -0
  96. package/frontend/dist/index.html +2 -2
  97. package/package.json +1 -1
  98. package/frontend/dist/assets/EndpointCopy-Dr-iLvMr.js +0 -2
  99. package/frontend/dist/assets/ServersPage-CxucQkR-.js +0 -39
  100. package/frontend/dist/assets/ServersPage-CxucQkR-.js.map +0 -1
  101. package/frontend/dist/assets/index-dacgBASO.js +0 -3
  102. package/frontend/dist/assets/index-dacgBASO.js.map +0 -1
  103. package/frontend/dist/assets/variableDetection-CUnJ5xdC.js +0 -16
  104. package/frontend/dist/assets/variableDetection-CUnJ5xdC.js.map +0 -1
@@ -1,3 +1,4 @@
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';
@@ -10,7 +11,7 @@ import { canServeToolRequests } from '../utils/serverAvailability.js';
10
11
  import { PrincipalRuntimeService } from './principalRuntimeService.js';
11
12
  import { UserContextService } from './userContextService.js';
12
13
  import { deleteCredentialBindings, missingCredentialError } from './credentialBindingService.js';
13
- import { CredentialBindingError, hasCredentialTemplate, validateCredentialTemplate, } from '../utils/credentialTemplate.js';
14
+ import { CredentialBindingError, hasCredentialTemplate, hasPersonalAuthentication, resolveCredentialArguments, validateCredentialTemplate, } from '../utils/credentialTemplate.js';
14
15
  import os from 'os';
15
16
  import path from 'path';
16
17
  import fs from 'fs';
@@ -172,13 +173,13 @@ const sanitizeArgs = (args) => {
172
173
  return args.map((arg) => {
173
174
  // Remove shell metacharacters that could be used for injection
174
175
  // Allow common flags (-f, --flag, -vvv) and paths
175
- 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'))) {
176
177
  return arg;
177
178
  }
178
179
  // For arguments with special characters, log a warning
179
180
  // Also block redirection operators (>, <, >>) and newlines
180
181
  logger.warn(`[proxychains] Potentially unsafe argument blocked: ${arg}`);
181
- return arg.replace(/[;&|`$(){}[\]!><\n\r]/g, '');
182
+ return arg.replace(/\{\{args\.[A-Za-z_][A-Za-z0-9_]*\}\}|[;&|`$(){}[\]!><\n\r]/g, (match) => match.startsWith('{{args.') ? match : '');
182
183
  });
183
184
  };
184
185
  /**
@@ -908,7 +909,7 @@ export const summarizeServerConnections = (infos) => {
908
909
  };
909
910
  export const getServerConnectionStats = () => {
910
911
  // Personal definitions have no shared connection whose readiness can be checked.
911
- return summarizeServerConnections(serverInfos.filter((info) => !hasCredentialTemplate(info.config)));
912
+ return summarizeServerConnections(serverInfos.filter((info) => !hasPersonalAuthentication(info.config)));
912
913
  };
913
914
  // Returns true if all enabled servers are connected
914
915
  export const connected = () => {
@@ -1207,7 +1208,7 @@ export const createTransportFromConfig = async (name, conf) => {
1207
1208
  if (conf.type === 'streamable-http') {
1208
1209
  const options = {};
1209
1210
  let headers = conf.headers
1210
- ? hasCredentialTemplate(conf)
1211
+ ? hasPersonalAuthentication(conf)
1211
1212
  ? conf.headers
1212
1213
  : replaceEnvVars(conf.headers, env)
1213
1214
  : {};
@@ -1233,14 +1234,15 @@ export const createTransportFromConfig = async (name, conf) => {
1233
1234
  headers,
1234
1235
  };
1235
1236
  }
1236
- options.fetch = requestAwareFetch;
1237
+ options.fetch =
1238
+ personalOAuthContexts.get(conf)?.wrapFetch(requestAwareFetch) || requestAwareFetch;
1237
1239
  transport = new StreamableHTTPClientTransport(new URL(conf.url || ''), options);
1238
1240
  }
1239
1241
  else if (conf.url) {
1240
1242
  // SSE transport
1241
1243
  const options = {};
1242
1244
  let headers = conf.headers
1243
- ? hasCredentialTemplate(conf)
1245
+ ? hasPersonalAuthentication(conf)
1244
1246
  ? conf.headers
1245
1247
  : replaceEnvVars(conf.headers, env)
1246
1248
  : {};
@@ -1259,7 +1261,7 @@ export const createTransportFromConfig = async (name, conf) => {
1259
1261
  if (Object.keys(headers).length > 0) {
1260
1262
  options.eventSourceInit = {
1261
1263
  headers,
1262
- fetch: requestAwareFetch,
1264
+ fetch: personalOAuthContexts.get(conf)?.wrapFetch(requestAwareFetch) || requestAwareFetch,
1263
1265
  };
1264
1266
  options.requestInit = {
1265
1267
  headers,
@@ -1267,10 +1269,11 @@ export const createTransportFromConfig = async (name, conf) => {
1267
1269
  }
1268
1270
  else {
1269
1271
  options.eventSourceInit = {
1270
- fetch: requestAwareFetch,
1272
+ fetch: personalOAuthContexts.get(conf)?.wrapFetch(requestAwareFetch) || requestAwareFetch,
1271
1273
  };
1272
1274
  }
1273
- options.fetch = requestAwareFetch;
1275
+ options.fetch =
1276
+ personalOAuthContexts.get(conf)?.wrapFetch(requestAwareFetch) || requestAwareFetch;
1274
1277
  transport = new SSEClientTransport(new URL(conf.url), options);
1275
1278
  }
1276
1279
  else if (conf.command) {
@@ -1305,12 +1308,13 @@ export const createTransportFromConfig = async (name, conf) => {
1305
1308
  transport = new StdioClientTransport({
1306
1309
  cwd: process.cwd(),
1307
1310
  command: finalCommand,
1308
- args: finalArgs,
1311
+ // Substitute after expansion and proxy argument cleaning so secrets stay literal.
1312
+ args: resolveCredentialArguments(conf, finalArgs),
1309
1313
  env: env,
1310
1314
  stderr: 'pipe',
1311
1315
  maxBufferSize: validateMaxBufferSize(conf.options?.maxBufferSize),
1312
1316
  });
1313
- if (transport.stderr && hasCredentialTemplate(conf)) {
1317
+ if (transport.stderr && hasPersonalAuthentication(conf)) {
1314
1318
  transport.stderr.on('data', () => undefined); // Personal upstream diagnostics can contain raw credentials.
1315
1319
  }
1316
1320
  else if (transport.stderr) {
@@ -1337,7 +1341,7 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
1337
1341
  if (!client) {
1338
1342
  throw new Error(`Client not found for server: ${serverInfo.name}`);
1339
1343
  }
1340
- if (hasCredentialTemplate(serverInfo.config)) {
1344
+ if (hasPersonalAuthentication(serverInfo.config)) {
1341
1345
  return client.callTool(toolParams, options || {});
1342
1346
  }
1343
1347
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
@@ -1514,7 +1518,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
1514
1518
  });
1515
1519
  continue;
1516
1520
  }
1517
- if (hasCredentialTemplate(expandedConf)) {
1521
+ if (hasPersonalAuthentication(expandedConf)) {
1518
1522
  validateCredentialTemplate(expandedConf);
1519
1523
  nextServerInfos.push({
1520
1524
  name,
@@ -2023,15 +2027,26 @@ export const getServersInfo = async (page, limit, user) => {
2023
2027
  const principal = (user ||
2024
2028
  UserContextService.getInstance().getCurrentUser());
2025
2029
  const presentedInfos = await Promise.all(filterServerInfos.map(async (info) => {
2026
- if (!hasCredentialTemplate(info.config) ||
2030
+ if (!hasPersonalAuthentication(info.config) ||
2027
2031
  info.enabled === false ||
2028
2032
  !principal?.username ||
2029
2033
  principal.credentialEligible === false)
2030
2034
  return info;
2035
+ let ownOAuth;
2031
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;
2032
2047
  const lease = await principalRuntimes.acquire(info.name, principal);
2033
2048
  try {
2034
- return { ...lease.info, config: info.config };
2049
+ return { ...lease.info, config: info.config, ...(ownOAuth ? { oauth: ownOAuth } : {}) };
2035
2050
  }
2036
2051
  finally {
2037
2052
  lease.release();
@@ -2040,6 +2055,7 @@ export const getServersInfo = async (page, limit, user) => {
2040
2055
  catch (error) {
2041
2056
  return {
2042
2057
  ...info,
2058
+ ...(ownOAuth ? { status: 'oauth_required', oauth: ownOAuth } : {}),
2043
2059
  error: error instanceof CredentialBindingError || error instanceof UpstreamRequestError
2044
2060
  ? error.message
2045
2061
  : 'Unable to prepare upstream server',
@@ -2102,6 +2118,8 @@ export const getServersInfo = async (page, limit, user) => {
2102
2118
  ? {
2103
2119
  ...(oauth
2104
2120
  ? {
2121
+ ...(oauth.identityMode ? { identityMode: oauth.identityMode } : {}),
2122
+ ...(oauth.connected ? { connected: true } : {}),
2105
2123
  authorizationUrl: oauth.authorizationUrl,
2106
2124
  state: oauth.state,
2107
2125
  ...(oauth.clientIdConfigured ? { clientIdConfigured: true } : {}),
@@ -2114,9 +2132,12 @@ export const getServersInfo = async (page, limit, user) => {
2114
2132
  config: resolvedType || serverConfig?.description || serverConfig?.command
2115
2133
  ? {
2116
2134
  ...(resolvedType ? { type: resolvedType } : {}),
2117
- ...(hasCredentialTemplate(serverConfig)
2135
+ ...(hasPersonalAuthentication(serverConfig)
2118
2136
  ? { credentialTemplate: validateCredentialTemplate(serverConfig) }
2119
2137
  : {}),
2138
+ ...(hasPersonalOAuth(serverConfig)
2139
+ ? { oauth: { identityMode: 'personal' } }
2140
+ : {}),
2120
2141
  ...(serverConfig?.description ? { description: serverConfig.description } : {}),
2121
2142
  // Expose command so the frontend can determine if reinstall is
2122
2143
  // supported (npx/uvx only). This is not a secret — it's the
@@ -2506,7 +2527,7 @@ const scheduleIdleShutdown = (serverInfo) => {
2506
2527
  clearIdleShutdown(serverInfo);
2507
2528
  if (!serverInfo.config?.startOnDemand ||
2508
2529
  !isStdioServer(serverInfo.config) ||
2509
- hasCredentialTemplate(serverInfo.config) ||
2530
+ hasPersonalAuthentication(serverInfo.config) ||
2510
2531
  serverInfo.status !== 'connected' ||
2511
2532
  !serverInfo.client ||
2512
2533
  serverInfo.activeToolCalls) {
@@ -2522,7 +2543,7 @@ const scheduleIdleShutdown = (serverInfo) => {
2522
2543
  const beginOnDemandToolCall = (serverInfo) => {
2523
2544
  if (!serverInfo.config?.startOnDemand ||
2524
2545
  !isStdioServer(serverInfo.config) ||
2525
- hasCredentialTemplate(serverInfo.config)) {
2546
+ hasPersonalAuthentication(serverInfo.config)) {
2526
2547
  return undefined;
2527
2548
  }
2528
2549
  clearIdleShutdown(serverInfo);
@@ -2670,7 +2691,7 @@ const isStdioServer = (conf) => !!conf && (conf.type === 'stdio' || (!conf.type
2670
2691
  */
2671
2692
  const primeOnDemandServers = (gate) => {
2672
2693
  const targets = serverInfos.filter((si) => si.config?.startOnDemand === true &&
2673
- !hasCredentialTemplate(si.config) &&
2694
+ !hasPersonalAuthentication(si.config) &&
2674
2695
  isStdioServer(si.config) &&
2675
2696
  si.tools.length === 0 &&
2676
2697
  si.enabled !== false);
@@ -2878,12 +2899,17 @@ const getMcpAppsRouteContext = async (sessionId, group) => {
2878
2899
  const eligible = (pinned?.pinnedServerInfos ?? []).filter(canServeToolRequests);
2879
2900
  return eligible.length > 0 ? { enabled: true, serverInfos: eligible } : { enabled: false };
2880
2901
  }
2881
- const { filteredServerInfos } = await getFilteredServerInfosForGroup(group);
2902
+ const { filteredServerInfos, serverConfigsByName } = await getFilteredServerInfosForGroup(group);
2882
2903
  const connectedServerInfos = filteredServerInfos.filter((serverInfo) => serverInfo.status === 'connected' && !!serverInfo.client);
2883
2904
  if (connectedServerInfos.length === 0) {
2884
2905
  return { enabled: false };
2885
2906
  }
2886
- if (connectedServerInfos.length === 1) {
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) {
2887
2913
  return {
2888
2914
  enabled: true,
2889
2915
  serverInfo: connectedServerInfos[0],
@@ -3023,7 +3049,7 @@ const projectToolForDownstream = (serverName, tool, appsRouteContext, serverConf
3023
3049
  return undefined;
3024
3050
  }
3025
3051
  const projectedTool = appsRouteContext.enabled ? tool : stripMcpAppsMetadata(tool);
3026
- // Single connected server: keep the existing raw (unqualified) name so
3052
+ // Single configured server: keep the existing raw (unqualified) name so
3027
3053
  // widgets that call back with the bare tool name continue to work.
3028
3054
  if (appsRouteContext.serverInfo) {
3029
3055
  return {
@@ -3031,7 +3057,7 @@ const projectToolForDownstream = (serverName, tool, appsRouteContext, serverConf
3031
3057
  name: normalizeToolNameForServer(serverName, projectedTool.name),
3032
3058
  };
3033
3059
  }
3034
- // Multiple connected servers (Apps-enabled) or Apps disabled: qualify the
3060
+ // Aggregate routes (Apps-enabled) or Apps disabled: qualify the
3035
3061
  // name so it can still be routed to the right server unambiguously.
3036
3062
  return {
3037
3063
  ...projectedTool,
@@ -3333,7 +3359,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
3333
3359
  keyId,
3334
3360
  keyName,
3335
3361
  sourceIp,
3336
- hasCredentialTemplate: hasCredentialTemplate(targetServerInfo.config),
3362
+ hasCredentialTemplate: hasPersonalAuthentication(targetServerInfo.config),
3337
3363
  });
3338
3364
  return await maybeCompressToolResult({
3339
3365
  content: [
@@ -3352,7 +3378,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
3352
3378
  // For servers with perSessionClient: true, use a per-session dedicated client
3353
3379
  let isolatedCtx;
3354
3380
  if (targetServerInfo.config?.perSessionClient &&
3355
- !hasCredentialTemplate(targetServerInfo.config)) {
3381
+ !hasPersonalAuthentication(targetServerInfo.config)) {
3356
3382
  const stateId = getToolStateId(targetServerInfo, sessionId);
3357
3383
  if (!stateId) {
3358
3384
  throw new Error(`Server '${targetServerInfo.name}' requires per-session client isolation and is not yet available on stateless MCP requests`);
@@ -3405,7 +3431,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
3405
3431
  keyId,
3406
3432
  keyName,
3407
3433
  sourceIp,
3408
- hasCredentialTemplate: hasCredentialTemplate(targetServerInfo.config),
3434
+ hasCredentialTemplate: hasPersonalAuthentication(targetServerInfo.config),
3409
3435
  errorMessage: result.isError ? 'Tool returned error response' : undefined,
3410
3436
  });
3411
3437
  return await maybeCompressToolResult(result, {
@@ -3512,7 +3538,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
3512
3538
  keyId,
3513
3539
  keyName,
3514
3540
  sourceIp,
3515
- hasCredentialTemplate: hasCredentialTemplate(serverInfo.config),
3541
+ hasCredentialTemplate: hasPersonalAuthentication(serverInfo.config),
3516
3542
  });
3517
3543
  return await maybeCompressToolResult({
3518
3544
  content: [
@@ -3530,7 +3556,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
3530
3556
  // Handle MCP servers
3531
3557
  // For servers with perSessionClient: true, use a per-session dedicated client
3532
3558
  let isolatedCtx;
3533
- if (serverInfo.config?.perSessionClient && !hasCredentialTemplate(serverInfo.config)) {
3559
+ if (serverInfo.config?.perSessionClient && !hasPersonalAuthentication(serverInfo.config)) {
3534
3560
  const stateId = getToolStateId(serverInfo, sessionId);
3535
3561
  if (!stateId) {
3536
3562
  throw new Error(`Server '${serverInfo.name}' requires per-session client isolation and is not yet available on stateless MCP requests`);
@@ -3568,7 +3594,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
3568
3594
  keyId,
3569
3595
  keyName,
3570
3596
  sourceIp,
3571
- hasCredentialTemplate: hasCredentialTemplate(serverInfo.config),
3597
+ hasCredentialTemplate: hasPersonalAuthentication(serverInfo.config),
3572
3598
  errorMessage: result.isError ? 'Tool returned error response' : undefined,
3573
3599
  });
3574
3600
  return await maybeCompressToolResult(result, {
@@ -3607,7 +3633,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
3607
3633
  keyId,
3608
3634
  keyName,
3609
3635
  sourceIp,
3610
- hasCredentialTemplate: hasCredentialTemplate(serverInfo?.config),
3636
+ hasCredentialTemplate: hasPersonalAuthentication(serverInfo?.config),
3611
3637
  // The reason is an internal diagnostic; the caller-facing message stays
3612
3638
  // identical for hidden and nonexistent targets.
3613
3639
  errorMessage: unavailable
@@ -4068,6 +4094,9 @@ const createPrincipalRuntime = async (name, resolvedConfig) => {
4068
4094
  }
4069
4095
  catch (error) {
4070
4096
  closeServerRuntime(info);
4097
+ if (hasPersonalOAuth(resolvedConfig) &&
4098
+ personalOAuthContexts.get(resolvedConfig)?.config.oauth.pendingAuthorization)
4099
+ throw personalAuthorizationRequired(name);
4071
4100
  const failure = createUpstreamRequestError(name, 'connect', error, resolvedConfig);
4072
4101
  logger.error(failure.message);
4073
4102
  throw failure;
@@ -4097,7 +4126,7 @@ export const getServerToolsForPrincipal = async (serverName, principal) => {
4097
4126
  }
4098
4127
  };
4099
4128
  const withPrincipalServers = (handler, operation) => async (request, extra) => {
4100
- if (!serverInfos.some((info) => hasCredentialTemplate(info.config)))
4129
+ if (!serverInfos.some((info) => hasPersonalAuthentication(info.config)))
4101
4130
  return handler(request, extra);
4102
4131
  // Built-in content does not require an upstream credential.
4103
4132
  if (operation === 'prompt') {
@@ -4134,7 +4163,7 @@ const withPrincipalServers = (handler, operation) => async (request, extra) => {
4134
4163
  }
4135
4164
  // A call to a known target must not spawn or keep unrelated personal children alive.
4136
4165
  const selected = target && (operation === 'tool' || operation === 'prompt') ? [target] : candidates;
4137
- const templated = selected.filter((info) => hasCredentialTemplate(info.config));
4166
+ const templated = selected.filter((info) => hasPersonalAuthentication(info.config));
4138
4167
  if (!templated.length)
4139
4168
  return handler(request, extra);
4140
4169
  const user = UserContextService.getInstance().getCurrentUser();
@@ -4147,7 +4176,9 @@ const withPrincipalServers = (handler, operation) => async (request, extra) => {
4147
4176
  if (!user?.username ||
4148
4177
  user.credentialEligible === false ||
4149
4178
  RequestContextService.getInstance().getKeyKindContext() === 'system')
4150
- throw missingCredentialError(info.name);
4179
+ throw hasPersonalOAuth(info.config)
4180
+ ? personalAuthorizationRequired(info.name)
4181
+ : missingCredentialError(info.name);
4151
4182
  const lease = await principalRuntimes.acquire(info.name, user);
4152
4183
  releases.push(lease.release);
4153
4184
  scoped.set(info.name, lease.info);
@@ -4176,7 +4207,7 @@ const withPrincipalServers = (handler, operation) => async (request, extra) => {
4176
4207
  let resolvedRequest = request;
4177
4208
  if (operation === 'tool' &&
4178
4209
  extra?.server &&
4179
- hasCredentialTemplate(scoped.get(extra.server)?.config)) {
4210
+ hasPersonalAuthentication(scoped.get(extra.server)?.config)) {
4180
4211
  const prefix = `${extra.server}${getNameSeparator()}`;
4181
4212
  if (typeof requestedName === 'string' && !requestedName.startsWith(prefix)) {
4182
4213
  resolvedRequest =