@samanhappy/mcphub 1.0.31 → 1.0.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/README.zh.md +12 -2
- package/dist/betterAuth.js +13 -5
- package/dist/betterAuth.js.map +1 -1
- package/dist/cli/commands/cache.js.map +1 -1
- package/dist/cli/commands/call.js +2 -2
- package/dist/cli/commands/call.js.map +1 -1
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/discover.js.map +1 -1
- package/dist/cli/commands/export.js.map +1 -1
- package/dist/cli/commands/groups.js.map +1 -1
- package/dist/cli/commands/install.js +1 -1
- package/dist/cli/commands/install.js.map +1 -1
- package/dist/cli/commands/keys.js.map +1 -1
- package/dist/cli/commands/login.js.map +1 -1
- package/dist/cli/commands/servers.js.map +1 -1
- package/dist/cli/commands/tools.js +2 -2
- package/dist/cli/commands/tools.js.map +1 -1
- package/dist/cli/context.js +1 -1
- package/dist/cli/context.js.map +1 -1
- package/dist/cli/http.js +3 -1
- package/dist/cli/http.js.map +1 -1
- package/dist/clients/openapi.js +498 -76
- package/dist/clients/openapi.js.map +1 -1
- package/dist/config/DaoConfigService.js +3 -2
- package/dist/config/DaoConfigService.js.map +1 -1
- package/dist/config/configManager.js +9 -8
- package/dist/config/configManager.js.map +1 -1
- package/dist/config/index.js +54 -8
- package/dist/config/index.js.map +1 -1
- package/dist/config/jwt.js +2 -1
- package/dist/config/jwt.js.map +1 -1
- package/dist/config/migrationUtils.js +46 -45
- package/dist/config/migrationUtils.js.map +1 -1
- package/dist/constants/oauthServerDefaults.js +5 -0
- package/dist/constants/oauthServerDefaults.js.map +1 -1
- package/dist/controllers/activityController.js +6 -5
- package/dist/controllers/activityController.js.map +1 -1
- package/dist/controllers/authController.js +5 -4
- package/dist/controllers/authController.js.map +1 -1
- package/dist/controllers/bearerKeyController.js +7 -6
- package/dist/controllers/bearerKeyController.js.map +1 -1
- package/dist/controllers/betterAuthController.js +2 -1
- package/dist/controllers/betterAuthController.js.map +1 -1
- package/dist/controllers/builtinPromptController.js +13 -5
- package/dist/controllers/builtinPromptController.js.map +1 -1
- package/dist/controllers/builtinResourceController.js +14 -6
- package/dist/controllers/builtinResourceController.js.map +1 -1
- package/dist/controllers/changelogController.js +5 -2
- package/dist/controllers/changelogController.js.map +1 -1
- package/dist/controllers/cloudController.js +11 -10
- package/dist/controllers/cloudController.js.map +1 -1
- package/dist/controllers/configController.js +4 -3
- package/dist/controllers/configController.js.map +1 -1
- package/dist/controllers/contextCostController.js +3 -2
- package/dist/controllers/contextCostController.js.map +1 -1
- package/dist/controllers/groupController.js +2 -1
- package/dist/controllers/groupController.js.map +1 -1
- package/dist/controllers/healthController.js +2 -1
- package/dist/controllers/healthController.js.map +1 -1
- package/dist/controllers/hostedInternalController.js +2 -1
- package/dist/controllers/hostedInternalController.js.map +1 -1
- package/dist/controllers/logController.js +5 -4
- package/dist/controllers/logController.js.map +1 -1
- package/dist/controllers/mcpbController.js +23 -20
- package/dist/controllers/mcpbController.js.map +1 -1
- package/dist/controllers/oauthCallbackController.js +54 -27
- package/dist/controllers/oauthCallbackController.js.map +1 -1
- package/dist/controllers/oauthClientController.js +7 -6
- package/dist/controllers/oauthClientController.js.map +1 -1
- package/dist/controllers/oauthDynamicRegistrationController.js +22 -4
- package/dist/controllers/oauthDynamicRegistrationController.js.map +1 -1
- package/dist/controllers/oauthServerController.js +62 -15
- package/dist/controllers/oauthServerController.js.map +1 -1
- package/dist/controllers/openApiController.js +7 -6
- package/dist/controllers/openApiController.js.map +1 -1
- package/dist/controllers/promptController.js +2 -1
- package/dist/controllers/promptController.js.map +1 -1
- package/dist/controllers/registryController.js +4 -3
- package/dist/controllers/registryController.js.map +1 -1
- package/dist/controllers/serverController.js +154 -65
- package/dist/controllers/serverController.js.map +1 -1
- package/dist/controllers/templateController.js +3 -2
- package/dist/controllers/templateController.js.map +1 -1
- package/dist/controllers/toolController.js +2 -1
- package/dist/controllers/toolController.js.map +1 -1
- package/dist/dao/DaoFactory.js +3 -2
- package/dist/dao/DaoFactory.js.map +1 -1
- package/dist/dao/ServerDao.js.map +1 -1
- package/dist/dao/UserConfigDaoDbImpl.js.map +1 -1
- package/dist/dao/UserDao.js.map +1 -1
- package/dist/dao/base/JsonFileBaseDao.js +3 -2
- package/dist/dao/base/JsonFileBaseDao.js.map +1 -1
- package/dist/dao/examples.js +35 -34
- package/dist/dao/examples.js.map +1 -1
- package/dist/db/connection.js +59 -58
- package/dist/db/connection.js.map +1 -1
- package/dist/db/index.js +2 -1
- package/dist/db/index.js.map +1 -1
- package/dist/db/repositories/ServerRepository.js +3 -3
- package/dist/db/repositories/ServerRepository.js.map +1 -1
- package/dist/db/repositories/SystemConfigRepository.js +2 -1
- package/dist/db/repositories/SystemConfigRepository.js.map +1 -1
- package/dist/db/repositories/UserConfigRepository.js.map +1 -1
- package/dist/db/repositories/VectorEmbeddingRepository.js +6 -5
- package/dist/db/repositories/VectorEmbeddingRepository.js.map +1 -1
- package/dist/index.js +28 -19
- package/dist/index.js.map +1 -1
- package/dist/middlewares/auth.js +4 -3
- package/dist/middlewares/auth.js.map +1 -1
- package/dist/middlewares/index.js +2 -1
- package/dist/middlewares/index.js.map +1 -1
- package/dist/middlewares/userContext.js +7 -6
- package/dist/middlewares/userContext.js.map +1 -1
- package/dist/models/OAuth.js +7 -6
- package/dist/models/OAuth.js.map +1 -1
- package/dist/models/User.js +13 -12
- package/dist/models/User.js.map +1 -1
- package/dist/routes/index.js +11 -10
- package/dist/routes/index.js.map +1 -1
- package/dist/scripts/dao-demo.js +28 -27
- package/dist/scripts/dao-demo.js.map +1 -1
- package/dist/server.js +36 -30
- package/dist/server.js.map +1 -1
- package/dist/services/activityLoggingService.js +3 -2
- package/dist/services/activityLoggingService.js.map +1 -1
- package/dist/services/authorizationService.js +28 -0
- package/dist/services/authorizationService.js.map +1 -0
- package/dist/services/betterAuthConfig.js +1 -4
- package/dist/services/betterAuthConfig.js.map +1 -1
- package/dist/services/betterAuthSession.js +8 -7
- package/dist/services/betterAuthSession.js.map +1 -1
- package/dist/services/changelogService.js +5 -2
- package/dist/services/changelogService.js.map +1 -1
- package/dist/services/cimdClientService.js +195 -0
- package/dist/services/cimdClientService.js.map +1 -0
- package/dist/services/cloudService.js +10 -9
- package/dist/services/cloudService.js.map +1 -1
- package/dist/services/contextCostService.js +6 -2
- package/dist/services/contextCostService.js.map +1 -1
- package/dist/services/groupService.js +8 -7
- package/dist/services/groupService.js.map +1 -1
- package/dist/services/hostedAuthService.js +3 -2
- package/dist/services/hostedAuthService.js.map +1 -1
- package/dist/services/hostedControlPlaneClient.js.map +1 -1
- package/dist/services/hostedEventSubscriber.js +9 -8
- package/dist/services/hostedEventSubscriber.js.map +1 -1
- package/dist/services/hostedRuntimeCatalogNames.js +1 -3
- package/dist/services/hostedRuntimeCatalogNames.js.map +1 -1
- package/dist/services/keepAliveService.js +7 -6
- package/dist/services/keepAliveService.js.map +1 -1
- package/dist/services/marketService.js +2 -1
- package/dist/services/marketService.js.map +1 -1
- package/dist/services/mcpOAuthProvider.js +22 -21
- package/dist/services/mcpOAuthProvider.js.map +1 -1
- package/dist/services/mcpService.js +239 -130
- package/dist/services/mcpService.js.map +1 -1
- package/dist/services/oauthClientRegistration.js +30 -29
- package/dist/services/oauthClientRegistration.js.map +1 -1
- package/dist/services/oauthServerService.js +39 -18
- package/dist/services/oauthServerService.js.map +1 -1
- package/dist/services/oauthService.js +16 -15
- package/dist/services/oauthService.js.map +1 -1
- package/dist/services/oauthSettingsStore.js +4 -3
- package/dist/services/oauthSettingsStore.js.map +1 -1
- package/dist/services/openApiGeneratorService.js +1 -3
- package/dist/services/openApiGeneratorService.js.map +1 -1
- package/dist/services/openApiToolStatsService.js +37 -0
- package/dist/services/openApiToolStatsService.js.map +1 -0
- package/dist/services/registry.js +2 -1
- package/dist/services/registry.js.map +1 -1
- package/dist/services/requestContextService.js.map +1 -1
- package/dist/services/serverConfigPresenter.js +85 -0
- package/dist/services/serverConfigPresenter.js.map +1 -0
- package/dist/services/smartRoutingService.js +6 -5
- package/dist/services/smartRoutingService.js.map +1 -1
- package/dist/services/sseService.js +52 -51
- package/dist/services/sseService.js.map +1 -1
- package/dist/services/templateService.js +105 -23
- package/dist/services/templateService.js.map +1 -1
- package/dist/services/toolResultCompressionService.js +8 -5
- package/dist/services/toolResultCompressionService.js.map +1 -1
- package/dist/services/upstreamOAuthDisconnectService.js +9 -6
- package/dist/services/upstreamOAuthDisconnectService.js.map +1 -1
- package/dist/services/userService.js +5 -4
- package/dist/services/userService.js.map +1 -1
- package/dist/services/vectorSearchService.js +126 -112
- package/dist/services/vectorSearchService.js.map +1 -1
- package/dist/utils/cacheUtils.js +5 -4
- package/dist/utils/cacheUtils.js.map +1 -1
- package/dist/utils/corsOrigin.js +42 -0
- package/dist/utils/corsOrigin.js.map +1 -0
- package/dist/utils/dbRetry.js +2 -5
- package/dist/utils/dbRetry.js.map +1 -1
- package/dist/utils/frontendShell.js +2 -1
- package/dist/utils/frontendShell.js.map +1 -1
- package/dist/utils/i18n.js.map +1 -1
- package/dist/utils/logger.js +170 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/migration.js +51 -50
- package/dist/utils/migration.js.map +1 -1
- package/dist/utils/oauthIssuer.js +57 -0
- package/dist/utils/oauthIssuer.js.map +1 -0
- package/dist/utils/openApiRequestBody.js +220 -0
- package/dist/utils/openApiRequestBody.js.map +1 -0
- package/dist/utils/parameterConversion.js +2 -1
- package/dist/utils/parameterConversion.js.map +1 -1
- package/dist/utils/path.js +9 -8
- package/dist/utils/path.js.map +1 -1
- package/dist/utils/processTree.js +47 -0
- package/dist/utils/processTree.js.map +1 -0
- package/dist/utils/proxyTrust.js.map +1 -1
- package/dist/utils/rateLimit.js +8 -0
- package/dist/utils/rateLimit.js.map +1 -1
- package/dist/utils/requireAdmin.js +15 -0
- package/dist/utils/requireAdmin.js.map +1 -0
- package/dist/utils/serverConfigPersistence.js +3 -0
- package/dist/utils/serverConfigPersistence.js.map +1 -1
- package/dist/utils/serverConfigValidation.js +13 -0
- package/dist/utils/serverConfigValidation.js.map +1 -0
- package/dist/utils/serverNameValidation.js +56 -0
- package/dist/utils/serverNameValidation.js.map +1 -0
- package/dist/utils/serverShutdown.js +3 -2
- package/dist/utils/serverShutdown.js.map +1 -1
- package/dist/utils/smartRouting.js +4 -3
- package/dist/utils/smartRouting.js.map +1 -1
- package/dist/utils/ssrf.js +23 -10
- package/dist/utils/ssrf.js.map +1 -1
- package/dist/utils/systemConfigCache.js +40 -2
- package/dist/utils/systemConfigCache.js.map +1 -1
- package/dist/utils/tokenCost.js.map +1 -1
- package/dist/utils/tokenTruncation.js +9 -4
- package/dist/utils/tokenTruncation.js.map +1 -1
- package/dist/utils/version.js +3 -2
- package/dist/utils/version.js.map +1 -1
- package/frontend/dist/assets/{ActivityPage-Dx53zxEM.js → ActivityPage-Du9Nh-Jy.js} +2 -2
- package/frontend/dist/assets/{ActivityPage-Dx53zxEM.js.map → ActivityPage-Du9Nh-Jy.js.map} +1 -1
- package/frontend/dist/assets/ConfirmDialog-3APts2Y9.js +2 -0
- package/frontend/dist/assets/ConfirmDialog-3APts2Y9.js.map +1 -0
- package/frontend/dist/assets/Dashboard-J4G-AZkI.js +2 -0
- package/frontend/dist/assets/{Dashboard-ZYVsSvOw.js.map → Dashboard-J4G-AZkI.js.map} +1 -1
- package/frontend/dist/assets/{DeleteDialog-DNe-zTEl.js → DeleteDialog-D9ycSRvn.js} +2 -2
- package/frontend/dist/assets/{DeleteDialog-DNe-zTEl.js.map → DeleteDialog-D9ycSRvn.js.map} +1 -1
- package/frontend/dist/assets/{EndpointCopy-ngxLPeDF.js → EndpointCopy-DFeLcU9A.js} +2 -2
- package/frontend/dist/assets/{EndpointCopy-ngxLPeDF.js.map → EndpointCopy-DFeLcU9A.js.map} +1 -1
- package/frontend/dist/assets/GroupsPage-CGMZBiXd.js +33 -0
- package/frontend/dist/assets/{GroupsPage-JPtupGpI.js.map → GroupsPage-CGMZBiXd.js.map} +1 -1
- package/frontend/dist/assets/{LoginPage-B6rU3Ugr.js → LoginPage-g5pJLBk0.js} +2 -2
- package/frontend/dist/assets/{LoginPage-B6rU3Ugr.js.map → LoginPage-g5pJLBk0.js.map} +1 -1
- package/frontend/dist/assets/{LogsPage-CXq382fb.js → LogsPage-Bkv7xhNf.js} +2 -2
- package/frontend/dist/assets/{LogsPage-CXq382fb.js.map → LogsPage-Bkv7xhNf.js.map} +1 -1
- package/frontend/dist/assets/MarketPage-WkLrckIr.js +3 -0
- package/frontend/dist/assets/MarketPage-WkLrckIr.js.map +1 -0
- package/frontend/dist/assets/{OAuthConsentPage-C-n6tv3-.js → OAuthConsentPage-CA35XoBK.js} +2 -2
- package/frontend/dist/assets/{OAuthConsentPage-C-n6tv3-.js.map → OAuthConsentPage-CA35XoBK.js.map} +1 -1
- package/frontend/dist/assets/{Pagination-DLSuQ8hw.js → Pagination-Dwko3g48.js} +2 -2
- package/frontend/dist/assets/{Pagination-DLSuQ8hw.js.map → Pagination-Dwko3g48.js.map} +1 -1
- package/frontend/dist/assets/{PromptsPage-BLpy76K4.js → PromptsPage-BuDdW69R.js} +2 -2
- package/frontend/dist/assets/{PromptsPage-BLpy76K4.js.map → PromptsPage-BuDdW69R.js.map} +1 -1
- package/frontend/dist/assets/{ResourcesPage-C5nynRqE.js → ResourcesPage-Cu-eD5e2.js} +2 -2
- package/frontend/dist/assets/{ResourcesPage-C5nynRqE.js.map → ResourcesPage-Cu-eD5e2.js.map} +1 -1
- package/frontend/dist/assets/{ServersPage-Ca5W9cPW.js → ServersPage-B1Np5x3N.js} +8 -8
- package/frontend/dist/assets/ServersPage-B1Np5x3N.js.map +1 -0
- package/frontend/dist/assets/{SettingsPage-DgFBjp_O.js → SettingsPage-BQN8-ZQh.js} +4 -4
- package/frontend/dist/assets/SettingsPage-BQN8-ZQh.js.map +1 -0
- package/frontend/dist/assets/{StatusDot-CmSkukx0.js → StatusDot-WyvgqplP.js} +2 -2
- package/frontend/dist/assets/{StatusDot-CmSkukx0.js.map → StatusDot-WyvgqplP.js.map} +1 -1
- package/frontend/dist/assets/{ToggleGroup-sG_2LQom.js → ToggleGroup-CmeedsZU.js} +2 -2
- package/frontend/dist/assets/{ToggleGroup-sG_2LQom.js.map → ToggleGroup-CmeedsZU.js.map} +1 -1
- package/frontend/dist/assets/UsersPage-DqDFrIIW.js +2 -0
- package/frontend/dist/assets/UsersPage-DqDFrIIW.js.map +1 -0
- package/frontend/dist/assets/contextCost-CilWqGWT.js +2 -0
- package/frontend/dist/assets/contextCost-CilWqGWT.js.map +1 -0
- package/frontend/dist/assets/{framework-vendor-I9Y6AgD5.js → framework-vendor-BHAQX05P.js} +3 -3
- package/frontend/dist/assets/{framework-vendor-I9Y6AgD5.js.map → framework-vendor-BHAQX05P.js.map} +1 -1
- package/frontend/dist/assets/{i18n-vendor-y9bLzIeh.js → i18n-vendor-CMcB7XUR.js} +2 -2
- package/frontend/dist/assets/{i18n-vendor-y9bLzIeh.js.map → i18n-vendor-CMcB7XUR.js.map} +1 -1
- package/frontend/dist/assets/{icons-vendor-BFl4A9fq.js → icons-vendor-HZHHWeTa.js} +2 -2
- package/frontend/dist/assets/{icons-vendor-BFl4A9fq.js.map → icons-vendor-HZHHWeTa.js.map} +1 -1
- package/frontend/dist/assets/index-BvU2-VT8.js +3 -0
- package/frontend/dist/assets/{index-CLugsCsy.js.map → index-BvU2-VT8.js.map} +1 -1
- package/frontend/dist/assets/index-DtrCVU1h.css +1 -0
- package/frontend/dist/assets/passwordValidation-DTLWgUy9.js +2 -0
- package/frontend/dist/assets/passwordValidation-DTLWgUy9.js.map +1 -0
- package/frontend/dist/assets/{resourceService-33rS8QGJ.js → resourceService-BFH4w47h.js} +2 -2
- package/frontend/dist/assets/{resourceService-33rS8QGJ.js.map → resourceService-BFH4w47h.js.map} +1 -1
- package/frontend/dist/assets/useCostData-CoM1SxdS.js +2 -0
- package/frontend/dist/assets/useCostData-CoM1SxdS.js.map +1 -0
- package/frontend/dist/assets/useSettingsData-DKrECrG1.js +2 -0
- package/frontend/dist/assets/{useSettingsData-fK2bv9oN.js.map → useSettingsData-DKrECrG1.js.map} +1 -1
- package/frontend/dist/assets/variableDetection-CIrYDjrq.js +16 -0
- package/frontend/dist/assets/variableDetection-CIrYDjrq.js.map +1 -0
- package/frontend/dist/index.html +5 -5
- package/package.json +2 -2
- package/frontend/dist/assets/ConfirmDialog-CYfXEuyU.js +0 -2
- package/frontend/dist/assets/ConfirmDialog-CYfXEuyU.js.map +0 -1
- package/frontend/dist/assets/Dashboard-ZYVsSvOw.js +0 -2
- package/frontend/dist/assets/GroupsPage-JPtupGpI.js +0 -33
- package/frontend/dist/assets/MarketPage-BGnQQyXK.js +0 -3
- package/frontend/dist/assets/MarketPage-BGnQQyXK.js.map +0 -1
- package/frontend/dist/assets/ServersPage-Ca5W9cPW.js.map +0 -1
- package/frontend/dist/assets/SettingsPage-DgFBjp_O.js.map +0 -1
- package/frontend/dist/assets/UsersPage-PHkFXLL8.js +0 -2
- package/frontend/dist/assets/UsersPage-PHkFXLL8.js.map +0 -1
- package/frontend/dist/assets/contextCost-D_iNNbhX.js +0 -2
- package/frontend/dist/assets/contextCost-D_iNNbhX.js.map +0 -1
- package/frontend/dist/assets/index-CGvImZ3F.css +0 -1
- package/frontend/dist/assets/index-CLugsCsy.js +0 -3
- package/frontend/dist/assets/useSettingsData-fK2bv9oN.js +0 -2
- package/frontend/dist/assets/variableDetection-ByJ_cnxa.js +0 -16
- package/frontend/dist/assets/variableDetection-ByJ_cnxa.js.map +0 -1
package/dist/clients/openapi.js
CHANGED
|
@@ -1,10 +1,57 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
2
4
|
import { CookieJar } from 'tough-cookie';
|
|
3
5
|
import SwaggerParser from '@apidevtools/swagger-parser';
|
|
4
6
|
import * as yaml from 'js-yaml';
|
|
5
|
-
import { assertSafeUrl, UnsafeUrlError } from '../utils/ssrf.js';
|
|
7
|
+
import { assertSafeUrl, UnsafeUrlError, createRedirectValidatingFetch } from '../utils/ssrf.js';
|
|
6
8
|
import { getUserDao } from '../dao/index.js';
|
|
7
9
|
import { sanitizeStringForLogging, createSafeJSON } from '../utils/serialization.js';
|
|
10
|
+
import { buildMultipartParts, encodeFormUrlEncoded, makeMultipartBodySchemaModelFriendly, selectRequestBodyContent, serializeMultipartBody, } from '../utils/openApiRequestBody.js';
|
|
11
|
+
// Encodes a substituted path parameter following OpenAPI's default
|
|
12
|
+
// `style: simple, explode: false`: primitives whole, arrays and objects part
|
|
13
|
+
// by part, joined with literal commas (#1083).
|
|
14
|
+
function encodePathParameterValue(value) {
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
return value.map((item) => encodeURIComponent(String(item))).join(',');
|
|
17
|
+
}
|
|
18
|
+
if (value !== null && typeof value === 'object') {
|
|
19
|
+
return Object.entries(value)
|
|
20
|
+
.map(([key, val]) => `${encodeURIComponent(key)},${encodeURIComponent(String(val))}`)
|
|
21
|
+
.join(',');
|
|
22
|
+
}
|
|
23
|
+
return encodeURIComponent(String(value));
|
|
24
|
+
}
|
|
25
|
+
function isPrintableAscii(value) {
|
|
26
|
+
return /^[\x20-\x7E]*$/.test(value);
|
|
27
|
+
}
|
|
28
|
+
// Strict base64 check: the alphabet plus canonical padding must round-trip
|
|
29
|
+
// through decode → encode unchanged. Buffer.from alone is too lenient (it
|
|
30
|
+
// silently drops invalid characters), which would misread raw secrets as
|
|
31
|
+
// encoded ones.
|
|
32
|
+
function tryDecodeBase64Utf8(value) {
|
|
33
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const decoded = Buffer.from(value, 'base64').toString('utf8');
|
|
37
|
+
const stripPadding = (input) => input.replace(/=+$/, '');
|
|
38
|
+
return stripPadding(Buffer.from(decoded, 'utf8').toString('base64')) === stripPadding(value)
|
|
39
|
+
? decoded
|
|
40
|
+
: null;
|
|
41
|
+
}
|
|
42
|
+
// HTTP Basic transmits base64("user:password"), but users reasonably enter the
|
|
43
|
+
// raw `user:pass` pair into the credentials field (#1079). Encode such values
|
|
44
|
+
// automatically; keep a value only when it decodes to printable
|
|
45
|
+
// `user:pass`-shaped text (contains ':'), the conservative test for a
|
|
46
|
+
// credential that was pre-encoded upstream. Anything ambiguous (e.g. a token
|
|
47
|
+
// without a colon) is treated as raw and encoded.
|
|
48
|
+
function toBasicAuthCredentials(credentials) {
|
|
49
|
+
const decoded = tryDecodeBase64Utf8(credentials);
|
|
50
|
+
if (decoded !== null && isPrintableAscii(decoded) && decoded.includes(':')) {
|
|
51
|
+
return credentials;
|
|
52
|
+
}
|
|
53
|
+
return Buffer.from(credentials, 'utf8').toString('base64');
|
|
54
|
+
}
|
|
8
55
|
export class OpenAPIClient {
|
|
9
56
|
constructor(config, options = {}) {
|
|
10
57
|
this.config = config;
|
|
@@ -20,20 +67,47 @@ export class OpenAPIClient {
|
|
|
20
67
|
if (!config.openapi?.url && !config.openapi?.schema) {
|
|
21
68
|
throw new Error('OpenAPI URL or schema is required');
|
|
22
69
|
}
|
|
23
|
-
// Initial baseUrl, will be updated from OpenAPI servers field in initialize()
|
|
70
|
+
// Initial baseUrl, will be updated from OpenAPI servers field in initialize().
|
|
71
|
+
// It is only used to resolve relative request paths (see callTool), never
|
|
72
|
+
// installed as the axios instance's default baseURL. Each request's
|
|
73
|
+
// effective URL is SSRF-validated explicitly before dispatch, so a
|
|
74
|
+
// user-derived default baseURL must not taint this.httpClient — CodeQL
|
|
75
|
+
// tracks a client's default baseURL as the host of every request it makes.
|
|
24
76
|
this.baseUrl = config.openapi?.url ? this.extractBaseUrl(config.openapi.url) : '';
|
|
25
77
|
this.securityConfig = config.openapi.security;
|
|
26
78
|
this.persistOAuth2Token = options.persistOAuth2Token;
|
|
27
|
-
this.httpClient =
|
|
28
|
-
|
|
79
|
+
this.httpClient = this.createHttpClient(config);
|
|
80
|
+
const mainCookie = this.applySecurityToClient(this.httpClient, this.securityConfig);
|
|
81
|
+
this.staticCookieHeader = mainCookie.staticCookieHeader;
|
|
82
|
+
const specSecurity = config.openapi?.specSecurity;
|
|
83
|
+
if (specSecurity && specSecurity.type !== 'none') {
|
|
84
|
+
// The dynamic OAuth2 client-credentials flow persists refreshed tokens
|
|
85
|
+
// back into `security`, so `specSecurity` only supports a static token.
|
|
86
|
+
if ((specSecurity.type === 'oauth2' && !specSecurity.oauth2?.token) ||
|
|
87
|
+
(specSecurity.type === 'openIdConnect' && !specSecurity.openIdConnect?.token)) {
|
|
88
|
+
throw new Error('openapi.specSecurity: oauth2/openIdConnect requires a pre-obtained token; dynamic token fetch is only supported for openapi.security');
|
|
89
|
+
}
|
|
90
|
+
this.specHttpClient = this.createHttpClient(config);
|
|
91
|
+
const specCookie = this.applySecurityToClient(this.specHttpClient, specSecurity);
|
|
92
|
+
this.specStaticCookieHeader = specCookie.staticCookieHeader;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Shared instance factory so the main client and the spec-download client
|
|
96
|
+
// (#1079) get identical base behavior: no redirects (credentials must never
|
|
97
|
+
// cross origins), OpenAPI array query serialization, and operator-supplied
|
|
98
|
+
// static headers.
|
|
99
|
+
createHttpClient(config) {
|
|
100
|
+
return axios.create({
|
|
29
101
|
timeout: config.options?.timeout || 30000,
|
|
30
102
|
maxRedirects: 0,
|
|
103
|
+
// Serialize array query params per OpenAPI's default `style: form, explode: true`
|
|
104
|
+
// (`id=a&id=b`) instead of axios's bracket form `id[]=a&id[]=b` (#1080).
|
|
105
|
+
paramsSerializer: { indexes: null },
|
|
31
106
|
headers: {
|
|
32
107
|
'Content-Type': 'application/json',
|
|
33
108
|
...config.headers,
|
|
34
109
|
},
|
|
35
110
|
});
|
|
36
|
-
this.setupSecurity();
|
|
37
111
|
}
|
|
38
112
|
extractBaseUrl(specUrl) {
|
|
39
113
|
try {
|
|
@@ -45,57 +119,68 @@ export class OpenAPIClient {
|
|
|
45
119
|
return '';
|
|
46
120
|
}
|
|
47
121
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
122
|
+
// Apply one OpenAPISecurityConfig onto a specific axios instance: default
|
|
123
|
+
// headers for header-based credentials, a request interceptor for query
|
|
124
|
+
// params, and a returned static cookie for the caller to manage (cookies
|
|
125
|
+
// cannot live in axios defaults). Both `security` and `specSecurity` (#1079)
|
|
126
|
+
// go through here so the two credential slots behave identically.
|
|
127
|
+
applySecurityToClient(client, securityConfig) {
|
|
128
|
+
if (!securityConfig || securityConfig.type === 'none') {
|
|
129
|
+
return {};
|
|
51
130
|
}
|
|
52
|
-
switch (
|
|
53
|
-
case 'apiKey':
|
|
54
|
-
if (
|
|
55
|
-
const { name, in: location, value } =
|
|
56
|
-
|
|
57
|
-
|
|
131
|
+
switch (securityConfig.type) {
|
|
132
|
+
case 'apiKey': {
|
|
133
|
+
if (securityConfig.apiKey) {
|
|
134
|
+
const { name, in: location, value } = securityConfig.apiKey;
|
|
135
|
+
// An empty/absent value means the credential has not been supplied
|
|
136
|
+
// yet ("needs credentials"); skip injection rather than sending a
|
|
137
|
+
// broken empty header/param/cookie (#1077).
|
|
138
|
+
if (location === 'header' && value) {
|
|
139
|
+
client.defaults.headers.common[name] = value;
|
|
58
140
|
}
|
|
59
|
-
else if (location === 'query') {
|
|
60
|
-
|
|
141
|
+
else if (location === 'query' && value) {
|
|
142
|
+
client.interceptors.request.use((config) => {
|
|
61
143
|
config.params = { ...config.params, [name]: value };
|
|
62
144
|
return config;
|
|
63
145
|
});
|
|
64
146
|
}
|
|
65
|
-
else if (location === 'cookie') {
|
|
66
|
-
|
|
147
|
+
else if (location === 'cookie' && value) {
|
|
148
|
+
return { staticCookieHeader: `${name}=${value}` };
|
|
67
149
|
}
|
|
68
150
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
151
|
+
return {};
|
|
152
|
+
}
|
|
153
|
+
case 'http': {
|
|
154
|
+
if (securityConfig.http) {
|
|
155
|
+
const { scheme, credentials } = securityConfig.http;
|
|
73
156
|
if (scheme === 'bearer' && credentials) {
|
|
74
|
-
|
|
157
|
+
client.defaults.headers.common['Authorization'] = `Bearer ${credentials}`;
|
|
75
158
|
}
|
|
76
159
|
else if (scheme === 'basic' && credentials) {
|
|
77
|
-
|
|
160
|
+
// Accept the raw `user:pass` form and pre-encoded base64 alike (#1079).
|
|
161
|
+
const trimmed = credentials.trim();
|
|
162
|
+
if (trimmed) {
|
|
163
|
+
client.defaults.headers.common['Authorization'] =
|
|
164
|
+
`Basic ${toBasicAuthCredentials(trimmed)}`;
|
|
165
|
+
}
|
|
78
166
|
}
|
|
79
167
|
}
|
|
80
|
-
|
|
168
|
+
return {};
|
|
169
|
+
}
|
|
81
170
|
case 'oauth2':
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
break;
|
|
171
|
+
this.setAuthorizationHeader(client, securityConfig.oauth2?.token);
|
|
172
|
+
return {};
|
|
86
173
|
case 'openIdConnect':
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
break;
|
|
174
|
+
this.setAuthorizationHeader(client, securityConfig.openIdConnect?.token);
|
|
175
|
+
return {};
|
|
91
176
|
}
|
|
92
177
|
}
|
|
93
|
-
setAuthorizationHeader(token) {
|
|
178
|
+
setAuthorizationHeader(client, token) {
|
|
94
179
|
if (token) {
|
|
95
|
-
|
|
180
|
+
client.defaults.headers.common['Authorization'] = 'Bearer ' + token;
|
|
96
181
|
return;
|
|
97
182
|
}
|
|
98
|
-
delete
|
|
183
|
+
delete client.defaults.headers.common['Authorization'];
|
|
99
184
|
}
|
|
100
185
|
getOAuth2Config() {
|
|
101
186
|
return this.securityConfig?.type === 'oauth2' ? this.securityConfig.oauth2 : undefined;
|
|
@@ -114,7 +199,7 @@ export class OpenAPIClient {
|
|
|
114
199
|
if (this.config.openapi?.security?.oauth2) {
|
|
115
200
|
this.config.openapi.security.oauth2 = oauth2;
|
|
116
201
|
}
|
|
117
|
-
this.setAuthorizationHeader(undefined);
|
|
202
|
+
this.setAuthorizationHeader(this.httpClient, undefined);
|
|
118
203
|
await this.persistOAuth2Token?.({ ...oauth2 });
|
|
119
204
|
return true;
|
|
120
205
|
}
|
|
@@ -142,7 +227,7 @@ export class OpenAPIClient {
|
|
|
142
227
|
if (this.config.openapi?.security?.oauth2) {
|
|
143
228
|
this.config.openapi.security.oauth2 = oauth2;
|
|
144
229
|
}
|
|
145
|
-
this.setAuthorizationHeader(token);
|
|
230
|
+
this.setAuthorizationHeader(this.httpClient, token);
|
|
146
231
|
await this.persistOAuth2Token?.(oauth2);
|
|
147
232
|
}
|
|
148
233
|
async fetchOAuth2ClientCredentialsToken(oauth2) {
|
|
@@ -159,9 +244,15 @@ export class OpenAPIClient {
|
|
|
159
244
|
if (Array.isArray(oauth2.scopes) && oauth2.scopes.length > 0) {
|
|
160
245
|
body.set('scope', oauth2.scopes.join(' '));
|
|
161
246
|
}
|
|
247
|
+
// Validate OAuth token endpoints with the same owner-scoped SSRF policy as
|
|
248
|
+
// specification and tool requests. This is especially important for the
|
|
249
|
+
// unsaved preview endpoint, which accepts the complete OpenAPI config.
|
|
250
|
+
const safeTokenUrl = await assertSafeUrl(oauth2.tokenUrl, {
|
|
251
|
+
allowInternal: this.allowInternalNetworks,
|
|
252
|
+
});
|
|
162
253
|
const response = await this.httpClient.request({
|
|
163
254
|
method: 'post',
|
|
164
|
-
url:
|
|
255
|
+
url: safeTokenUrl,
|
|
165
256
|
baseURL: undefined,
|
|
166
257
|
headers: {
|
|
167
258
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -184,12 +275,12 @@ export class OpenAPIClient {
|
|
|
184
275
|
return undefined;
|
|
185
276
|
}
|
|
186
277
|
if (this.hasValidOAuth2Token(oauth2)) {
|
|
187
|
-
this.setAuthorizationHeader(oauth2.token);
|
|
278
|
+
this.setAuthorizationHeader(this.httpClient, oauth2.token);
|
|
188
279
|
return oauth2.token;
|
|
189
280
|
}
|
|
190
281
|
if (!oauth2.tokenUrl || !oauth2.clientId) {
|
|
191
282
|
if (oauth2.token) {
|
|
192
|
-
this.setAuthorizationHeader(oauth2.token);
|
|
283
|
+
this.setAuthorizationHeader(this.httpClient, oauth2.token);
|
|
193
284
|
}
|
|
194
285
|
return oauth2.token;
|
|
195
286
|
}
|
|
@@ -214,33 +305,46 @@ export class OpenAPIClient {
|
|
|
214
305
|
// document download (for url configs) and is ready for later API calls.
|
|
215
306
|
// No-op when no OAuth2 security is configured.
|
|
216
307
|
await this.ensureOAuth2AccessToken();
|
|
308
|
+
// Reset the external-$ref guard marker so a rejection from THIS
|
|
309
|
+
// dereference pass is never confused with a stale one.
|
|
310
|
+
this.refGuardRejection = undefined;
|
|
217
311
|
// Parse and dereference the OpenAPI specification
|
|
218
312
|
if (this.config.openapi?.url) {
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
// the main document so its (unauthenticated) headers never see the
|
|
224
|
-
// credentials; external $ref resolution still uses that resolver and
|
|
225
|
-
// therefore receives no auth by default.
|
|
313
|
+
// SwaggerParser's own resolver is bypassed for the main document so
|
|
314
|
+
// its (unauthenticated) headers never see the credentials; external
|
|
315
|
+
// $ref resolution still uses that resolver and therefore receives no
|
|
316
|
+
// auth by default.
|
|
226
317
|
const specUrl = this.config.openapi.url;
|
|
227
|
-
await assertSafeUrl(specUrl, {
|
|
318
|
+
const safeSpecUrl = await assertSafeUrl(specUrl, {
|
|
319
|
+
allowInternal: this.allowInternalNetworks,
|
|
320
|
+
});
|
|
228
321
|
const requestConfig = {
|
|
229
322
|
responseType: 'text',
|
|
230
323
|
transformResponse: [(data) => data],
|
|
231
324
|
};
|
|
325
|
+
// Download through the client whose credentials match the spec
|
|
326
|
+
// endpoint: the dedicated specHttpClient when `specSecurity` names one
|
|
327
|
+
// (#1079), otherwise the main client whose defaults already carry
|
|
328
|
+
// `security` (and the apiKey-in-query interceptor). Uses maxRedirects:
|
|
329
|
+
// 0 so credentials are never forwarded across a cross-origin redirect;
|
|
330
|
+
// external $ref resolution keeps using the unauthenticated resolver
|
|
331
|
+
// from guardedRefResolveOptions below.
|
|
332
|
+
const specClient = this.specHttpClient ?? this.httpClient;
|
|
333
|
+
const specCookie = this.specHttpClient
|
|
334
|
+
? this.specStaticCookieHeader
|
|
335
|
+
: this.staticCookieHeader;
|
|
232
336
|
// The static apiKey.in:'cookie' value is otherwise only injected in
|
|
233
|
-
// callTool; apply it here
|
|
234
|
-
if (
|
|
235
|
-
requestConfig.headers = { Cookie:
|
|
337
|
+
// callTool; apply it here so cookie-protected spec URLs load.
|
|
338
|
+
if (specCookie) {
|
|
339
|
+
requestConfig.headers = { Cookie: specCookie };
|
|
236
340
|
}
|
|
237
|
-
const response = await
|
|
341
|
+
const response = await specClient.get(safeSpecUrl, requestConfig);
|
|
238
342
|
const raw = typeof response.data === 'string' ? response.data : String(response.data);
|
|
239
|
-
this.spec = (await SwaggerParser.dereference(
|
|
343
|
+
this.spec = (await SwaggerParser.dereference(safeSpecUrl, this.parseSpecDocument(raw), this.guardedRefResolveOptions()));
|
|
240
344
|
}
|
|
241
345
|
else if (this.config.openapi?.schema) {
|
|
242
346
|
// For schema object, we need to pass it as a cloned object
|
|
243
|
-
this.spec = (await SwaggerParser.dereference(JSON.parse(JSON.stringify(this.config.openapi.schema))));
|
|
347
|
+
this.spec = (await SwaggerParser.dereference(JSON.parse(JSON.stringify(this.config.openapi.schema)), this.guardedRefResolveOptions()));
|
|
244
348
|
}
|
|
245
349
|
else {
|
|
246
350
|
throw new Error('Either OpenAPI URL or schema must be provided');
|
|
@@ -255,6 +359,12 @@ export class OpenAPIClient {
|
|
|
255
359
|
if (error instanceof UnsafeUrlError) {
|
|
256
360
|
throw error;
|
|
257
361
|
}
|
|
362
|
+
// SwaggerParser wraps resolver failures (including our SSRF guard
|
|
363
|
+
// rejections) in its own ResolverError and drops the cause — surface the
|
|
364
|
+
// real UnsafeUrlError when the guarded $ref resolver flagged one.
|
|
365
|
+
if (this.refGuardRejection) {
|
|
366
|
+
throw this.refGuardRejection;
|
|
367
|
+
}
|
|
258
368
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
259
369
|
throw new Error(`Failed to load OpenAPI specification: ${sanitizeStringForLogging(errorMessage)}`);
|
|
260
370
|
}
|
|
@@ -270,6 +380,57 @@ export class OpenAPIClient {
|
|
|
270
380
|
return yaml.load(raw);
|
|
271
381
|
}
|
|
272
382
|
}
|
|
383
|
+
// SwaggerParser options that replace its built-in resolvers so EVERY external
|
|
384
|
+
// $ref target is validated by the SSRF guard before any request leaves the
|
|
385
|
+
// process. Without this, a non-admin-supplied spec could point $refs at
|
|
386
|
+
// internal addresses (IMDS, RFC1918, loopback) or local files and have their
|
|
387
|
+
// contents embedded into the dereferenced schema served back to clients.
|
|
388
|
+
// Admin-owned servers keep the same allowInternal escape hatch as the main
|
|
389
|
+
// document download and callTool.
|
|
390
|
+
guardedRefResolveOptions() {
|
|
391
|
+
const rejectUnsafe = (err) => {
|
|
392
|
+
this.refGuardRejection = err;
|
|
393
|
+
throw err;
|
|
394
|
+
};
|
|
395
|
+
return {
|
|
396
|
+
resolve: {
|
|
397
|
+
http: {
|
|
398
|
+
order: 1,
|
|
399
|
+
canRead: (file) => /^https?:\/\//i.test(file.url),
|
|
400
|
+
read: async (file) => {
|
|
401
|
+
let safeRefUrl;
|
|
402
|
+
try {
|
|
403
|
+
safeRefUrl = await assertSafeUrl(file.url, {
|
|
404
|
+
allowInternal: this.allowInternalNetworks,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
rejectUnsafe(error);
|
|
409
|
+
return '';
|
|
410
|
+
}
|
|
411
|
+
// Plain fetch, NOT this.httpClient: default auth headers and
|
|
412
|
+
// interceptors must never be forwarded cross-origin to $ref
|
|
413
|
+
// targets (#1044). Redirects are followed manually with every hop
|
|
414
|
+
// re-validated by the guard.
|
|
415
|
+
const safeFetch = createRedirectValidatingFetch((url, init) => fetch(url, init), this.allowInternalNetworks);
|
|
416
|
+
const response = await safeFetch(safeRefUrl, { method: 'GET', headers: {} });
|
|
417
|
+
const buf = await response.arrayBuffer();
|
|
418
|
+
return new TextDecoder().decode(buf);
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
file: {
|
|
422
|
+
order: 1,
|
|
423
|
+
canRead: true,
|
|
424
|
+
read: async (file) => {
|
|
425
|
+
if (!this.allowInternalNetworks) {
|
|
426
|
+
rejectUnsafe(new UnsafeUrlError(`Blocked local file reference in specification: ${sanitizeStringForLogging(file.url)}`));
|
|
427
|
+
}
|
|
428
|
+
return readFile(new URL(file.url), 'utf8');
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
}
|
|
273
434
|
generateOperationName(method, path) {
|
|
274
435
|
// Clean path, remove parameter brackets and special characters
|
|
275
436
|
const cleanPath = path
|
|
@@ -321,8 +482,6 @@ export class OpenAPIClient {
|
|
|
321
482
|
this.baseUrl = `${originalUrl.protocol}//${originalUrl.host}/${serverUrl}`;
|
|
322
483
|
}
|
|
323
484
|
}
|
|
324
|
-
// Update HTTP client's baseURL
|
|
325
|
-
this.httpClient.defaults.baseURL = this.baseUrl;
|
|
326
485
|
}
|
|
327
486
|
extractTools() {
|
|
328
487
|
if (!this.spec?.paths) {
|
|
@@ -364,15 +523,30 @@ export class OpenAPIClient {
|
|
|
364
523
|
operationName = uniqueName;
|
|
365
524
|
}
|
|
366
525
|
generatedNames.add(operationName);
|
|
526
|
+
// Resolve the operation's declared request-body content type once so
|
|
527
|
+
// schema advertisement below and outgoing serialization in callTool
|
|
528
|
+
// stay symmetric (#1078).
|
|
529
|
+
const declaredRequestBody = operation.requestBody && 'content' in operation.requestBody
|
|
530
|
+
? operation.requestBody
|
|
531
|
+
: undefined;
|
|
532
|
+
const requestBodySelection = selectRequestBodyContent(declaredRequestBody);
|
|
533
|
+
let description = operation.summary || operation.description || `${method.toUpperCase()} ${path}`;
|
|
534
|
+
// A body whose content type the hub cannot serialize used to produce a
|
|
535
|
+
// silent zero-argument tool that models would retry forever. Mark such
|
|
536
|
+
// operations visibly in the description instead (#1078).
|
|
537
|
+
if (declaredRequestBody && !requestBodySelection) {
|
|
538
|
+
const declaredTypes = Object.keys(declaredRequestBody.content ?? {}).join(', ');
|
|
539
|
+
description += ` [Unsupported request body content type(s): ${declaredTypes} — only application/json, application/x-www-form-urlencoded and multipart/form-data can be sent by MCPHub.]`;
|
|
540
|
+
}
|
|
367
541
|
const tool = {
|
|
368
542
|
name: operationName,
|
|
369
|
-
description
|
|
543
|
+
description,
|
|
370
544
|
// SwaggerParser.dereference turns recursive $ref schemas into live
|
|
371
545
|
// circular references on the dereferenced spec objects. generateInputSchema
|
|
372
546
|
// references those objects directly, so without sanitization every
|
|
373
547
|
// downstream serializer (tokenCost, getServerConfig, MCP ListTools,
|
|
374
548
|
// embeddings) throws "Converting circular structure to JSON". See #959.
|
|
375
|
-
inputSchema: createSafeJSON(this.generateInputSchema(operation, path, method)),
|
|
549
|
+
inputSchema: createSafeJSON(this.generateInputSchema(operation, path, method, requestBodySelection)),
|
|
376
550
|
operationId: operation.operationId || operationName,
|
|
377
551
|
method: method,
|
|
378
552
|
path,
|
|
@@ -384,7 +558,7 @@ export class OpenAPIClient {
|
|
|
384
558
|
}
|
|
385
559
|
}
|
|
386
560
|
}
|
|
387
|
-
generateInputSchema(operation, _path, _method) {
|
|
561
|
+
generateInputSchema(operation, _path, _method, requestBodySelection) {
|
|
388
562
|
const schema = {
|
|
389
563
|
type: 'object',
|
|
390
564
|
properties: {},
|
|
@@ -425,12 +599,18 @@ export class OpenAPIClient {
|
|
|
425
599
|
}
|
|
426
600
|
}
|
|
427
601
|
}
|
|
428
|
-
// Handle request body
|
|
429
|
-
|
|
602
|
+
// Handle request body. The advertised schema must match what callTool can
|
|
603
|
+
// serialize, so key off the same resolved content type selection (#1078):
|
|
604
|
+
// JSON and urlencoded bodies are exposed as-is; multipart bodies are
|
|
605
|
+
// rewritten so binary fields accept base64-encoded strings.
|
|
606
|
+
if (operation.requestBody && 'content' in operation.requestBody && requestBodySelection) {
|
|
430
607
|
const requestBody = operation.requestBody;
|
|
431
|
-
const
|
|
432
|
-
if (
|
|
433
|
-
properties['body'] =
|
|
608
|
+
const selectedSchema = requestBodySelection.mediaType.schema;
|
|
609
|
+
if (selectedSchema) {
|
|
610
|
+
properties['body'] =
|
|
611
|
+
requestBodySelection.contentType === 'multipart/form-data'
|
|
612
|
+
? makeMultipartBodySchemaModelFriendly(selectedSchema)
|
|
613
|
+
: selectedSchema;
|
|
434
614
|
if (requestBody.required) {
|
|
435
615
|
required.push('body');
|
|
436
616
|
}
|
|
@@ -466,9 +646,14 @@ export class OpenAPIClient {
|
|
|
466
646
|
const pathParams = tool.parameters?.filter((p) => p.in === 'path') || [];
|
|
467
647
|
for (const param of pathParams) {
|
|
468
648
|
const value = args[param.name];
|
|
469
|
-
if (value
|
|
470
|
-
|
|
649
|
+
if (value === undefined || value === null) {
|
|
650
|
+
// Path parameters are required by the OpenAPI spec; fail fast rather
|
|
651
|
+
// than sending a request whose `{placeholder}` can only 404 (#1083).
|
|
652
|
+
throw new Error(`Required path parameter '${param.name}' is missing`);
|
|
471
653
|
}
|
|
654
|
+
// Values come from model output, so encode them to keep URL-significant
|
|
655
|
+
// characters ('/', '?', '#', '%') from changing the endpoint (#1083).
|
|
656
|
+
url = url.replace(`{${param.name}}`, encodePathParameterValue(value));
|
|
472
657
|
}
|
|
473
658
|
// Build query parameters
|
|
474
659
|
const queryParams = {};
|
|
@@ -485,9 +670,41 @@ export class OpenAPIClient {
|
|
|
485
670
|
url,
|
|
486
671
|
params: queryParams,
|
|
487
672
|
};
|
|
488
|
-
// Add request body if applicable
|
|
489
|
-
|
|
490
|
-
|
|
673
|
+
// Add request body if applicable. Key off the operation's own spec rather
|
|
674
|
+
// than a method allowlist: some APIs declare required bodies on DELETE
|
|
675
|
+
// (bulk deletes), and RFC 9110 §9.3.5 permits content when the origin
|
|
676
|
+
// server has indicated support for it — which a requestBody declaration
|
|
677
|
+
// in its OpenAPI document is. Keeps schema advertisement and sending
|
|
678
|
+
// symmetric (#1084). The body is serialized according to the content type
|
|
679
|
+
// resolved from the same spec the tool schema was built from (#1078).
|
|
680
|
+
let requestBodyContentType;
|
|
681
|
+
if (args.body !== undefined && tool.requestBody) {
|
|
682
|
+
const selection = selectRequestBodyContent(tool.requestBody);
|
|
683
|
+
if (!selection) {
|
|
684
|
+
const declaredTypes = Object.keys(tool.requestBody.content ?? {}).join(', ');
|
|
685
|
+
throw new Error(`Tool '${toolName}' declares a request body with unsupported content type(s): ${declaredTypes}. Only application/json, application/x-www-form-urlencoded and multipart/form-data can be sent by MCPHub.`);
|
|
686
|
+
}
|
|
687
|
+
switch (selection.contentType) {
|
|
688
|
+
case 'application/json':
|
|
689
|
+
requestConfig.data = args.body;
|
|
690
|
+
break;
|
|
691
|
+
case 'application/x-www-form-urlencoded':
|
|
692
|
+
requestConfig.data = encodeFormUrlEncoded(args.body);
|
|
693
|
+
requestBodyContentType = 'application/x-www-form-urlencoded';
|
|
694
|
+
break;
|
|
695
|
+
case 'multipart/form-data': {
|
|
696
|
+
const boundary = `----MCPHubBoundary${randomUUID().replace(/-/g, '')}`;
|
|
697
|
+
// The spec is dereferenced before tools are built, so the schema
|
|
698
|
+
// cannot still be a $ref here.
|
|
699
|
+
const bodySchema = selection.mediaType.schema;
|
|
700
|
+
const parts = buildMultipartParts(args.body, bodySchema);
|
|
701
|
+
requestConfig.data = serializeMultipartBody(parts, boundary);
|
|
702
|
+
// The boundary must travel with the header, so it is set explicitly
|
|
703
|
+
// per request rather than left to axios.
|
|
704
|
+
requestBodyContentType = `multipart/form-data; boundary=${boundary}`;
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
491
708
|
}
|
|
492
709
|
// Collect all headers to be sent
|
|
493
710
|
const allHeaders = {};
|
|
@@ -507,6 +724,11 @@ export class OpenAPIClient {
|
|
|
507
724
|
}
|
|
508
725
|
}
|
|
509
726
|
}
|
|
727
|
+
// Form and multipart bodies override the client-wide JSON Content-Type
|
|
728
|
+
// default for this request only (#1078).
|
|
729
|
+
if (requestBodyContentType) {
|
|
730
|
+
allHeaders['Content-Type'] = requestBodyContentType;
|
|
731
|
+
}
|
|
510
732
|
// Set headers if any were collected
|
|
511
733
|
if (Object.keys(allHeaders).length > 0) {
|
|
512
734
|
requestConfig.headers = allHeaders;
|
|
@@ -517,15 +739,34 @@ export class OpenAPIClient {
|
|
|
517
739
|
// final URL rather than trusting either alone.
|
|
518
740
|
resolvedTarget = null;
|
|
519
741
|
try {
|
|
520
|
-
|
|
742
|
+
// Join the operation path onto the server-declared base path with
|
|
743
|
+
// append semantics, matching axios's combineURLs before #937 and the
|
|
744
|
+
// OpenAPI servers+paths model: `new URL` reference resolution would
|
|
745
|
+
// drop the base path for root-absolute paths ('/ping' against
|
|
746
|
+
// 'http://host/api' resolves to '/ping'), 404-ing every tool call
|
|
747
|
+
// (#1098). Absolute URLs bypass the join and resolve as-is.
|
|
748
|
+
let joinedPath = String(requestConfig.url ?? '/');
|
|
749
|
+
let resolveBase = this.baseUrl || undefined;
|
|
750
|
+
if (resolveBase && !/^https?:\/\//i.test(joinedPath)) {
|
|
751
|
+
const parsedBase = new URL(resolveBase);
|
|
752
|
+
const basePath = parsedBase.pathname.replace(/\/+$/, '');
|
|
753
|
+
if (basePath) {
|
|
754
|
+
joinedPath = `${basePath}/${joinedPath.replace(/^\/+/, '')}`;
|
|
755
|
+
}
|
|
756
|
+
resolveBase = parsedBase.origin;
|
|
757
|
+
}
|
|
758
|
+
resolvedTarget = new URL(joinedPath, resolveBase);
|
|
521
759
|
}
|
|
522
760
|
catch {
|
|
523
761
|
// relative path with no base — no host to validate; axios surfaces the error
|
|
524
762
|
}
|
|
525
763
|
if (resolvedTarget) {
|
|
526
|
-
await assertSafeUrl(resolvedTarget.href, {
|
|
764
|
+
const safeTargetUrl = await assertSafeUrl(resolvedTarget.href, {
|
|
527
765
|
allowInternal: this.allowInternalNetworks,
|
|
528
766
|
});
|
|
767
|
+
const safeTarget = new URL(safeTargetUrl);
|
|
768
|
+
requestConfig.baseURL = safeTarget.origin;
|
|
769
|
+
requestConfig.url = `${safeTarget.pathname}${safeTarget.search}`;
|
|
529
770
|
}
|
|
530
771
|
const cookieSessionEnabled = this.isCookieSessionEnabled(sessionId);
|
|
531
772
|
// Inject cookies when either the dynamic session store is active for this
|
|
@@ -548,9 +789,7 @@ export class OpenAPIClient {
|
|
|
548
789
|
// a 3xx login redirect is surfaced as an error; 4xx logout responses
|
|
549
790
|
// may expire cookies. Capture before retry/rethrow so the jar stays
|
|
550
791
|
// current and the 401-retry path can re-inject updated cookies.
|
|
551
|
-
if (this.isCookieSessionEnabled(sessionId) &&
|
|
552
|
-
resolvedTarget &&
|
|
553
|
-
error.response) {
|
|
792
|
+
if (this.isCookieSessionEnabled(sessionId) && resolvedTarget && error.response) {
|
|
554
793
|
this.captureResponseCookies(error.response, resolvedTarget.href, sessionId);
|
|
555
794
|
}
|
|
556
795
|
if (attemptedUpstreamRequest &&
|
|
@@ -595,8 +834,191 @@ export class OpenAPIClient {
|
|
|
595
834
|
getSpec() {
|
|
596
835
|
return this.spec;
|
|
597
836
|
}
|
|
837
|
+
// ── Declared-security resolution (#1077) ─────────────────────────────────
|
|
838
|
+
//
|
|
839
|
+
// The import path reads the effective `security` requirement the spec
|
|
840
|
+
// declares and maps it onto MCPHub's OpenAPISecurityConfig so the form can
|
|
841
|
+
// prefill type/scheme/name fields. Only structural fields are ever produced
|
|
842
|
+
// — a spec cannot contain the secret, so value / credentials / token stay
|
|
843
|
+
// empty for the user.
|
|
844
|
+
/**
|
|
845
|
+
* Resolve the effective security requirement the (already parsed) spec
|
|
846
|
+
* declares and map it onto MCPHub's security model for form prefill.
|
|
847
|
+
*
|
|
848
|
+
* OpenAPI 3.x rules applied here:
|
|
849
|
+
* - an operation-level `security` overrides the root-level one for that
|
|
850
|
+
* operation; operations without one inherit the root requirement;
|
|
851
|
+
* - `security: []` explicitly disables auth for that scope;
|
|
852
|
+
* - each requirement is an OR-list of named schemes (from
|
|
853
|
+
* `components.securitySchemes`); a single scheme may be repeated in an
|
|
854
|
+
* AND-list with its required scopes.
|
|
855
|
+
*
|
|
856
|
+
* The first requirement whose scheme maps onto MCPHub's model (apiKey,
|
|
857
|
+
* http/basic|bearer, oauth2, openIdConnect) is returned as `prefill`.
|
|
858
|
+
*/
|
|
859
|
+
getDeclaredSecurity() {
|
|
860
|
+
const none = {
|
|
861
|
+
declared: false,
|
|
862
|
+
supported: false,
|
|
863
|
+
summary: '',
|
|
864
|
+
alternatives: 0,
|
|
865
|
+
requiresCredentials: false,
|
|
866
|
+
};
|
|
867
|
+
if (!this.spec) {
|
|
868
|
+
return none;
|
|
869
|
+
}
|
|
870
|
+
const rootSecurity = Array.isArray(this.spec.security) ? this.spec.security : undefined;
|
|
871
|
+
// An explicit operation-level requirement is the most specific declaration
|
|
872
|
+
// in the document (it overrides the root default for that operation), so it
|
|
873
|
+
// wins for prefill; the root requirement is the fallback default.
|
|
874
|
+
const effective = this.firstOperationSecurity() ??
|
|
875
|
+
(rootSecurity && rootSecurity.length > 0 ? rootSecurity : undefined);
|
|
876
|
+
if (!effective || effective.length === 0) {
|
|
877
|
+
return none;
|
|
878
|
+
}
|
|
879
|
+
const schemes = this.spec.components?.securitySchemes ?? {};
|
|
880
|
+
return this.mapSecurityRequirement(effective, schemes);
|
|
881
|
+
}
|
|
882
|
+
// Per OpenAPI 3.x, an operation without a `security` field inherits the
|
|
883
|
+
// root-level requirement. Return the first operation that declares one — the
|
|
884
|
+
// most specific security statement in the document — so the prefill reflects
|
|
885
|
+
// what operations actually require rather than only the root default.
|
|
886
|
+
firstOperationSecurity() {
|
|
887
|
+
if (!this.spec?.paths) {
|
|
888
|
+
return undefined;
|
|
889
|
+
}
|
|
890
|
+
const methods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'];
|
|
891
|
+
for (const pathItem of Object.values(this.spec.paths)) {
|
|
892
|
+
if (!pathItem) {
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
for (const method of methods) {
|
|
896
|
+
const operation = pathItem[method];
|
|
897
|
+
if (operation?.security && operation.security.length > 0) {
|
|
898
|
+
return operation.security;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return undefined;
|
|
903
|
+
}
|
|
904
|
+
mapSecurityRequirement(requirement, schemes) {
|
|
905
|
+
let firstUnsupported;
|
|
906
|
+
for (const alternative of requirement) {
|
|
907
|
+
for (const schemeName of Object.keys(alternative)) {
|
|
908
|
+
const scheme = schemes[schemeName];
|
|
909
|
+
if (!scheme || '$ref' in scheme) {
|
|
910
|
+
// Unknown name or an unresolved reference — try the next alternative.
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
const mapped = this.mapSecurityScheme(schemeName, scheme);
|
|
914
|
+
if (mapped.ok) {
|
|
915
|
+
return {
|
|
916
|
+
declared: true,
|
|
917
|
+
supported: true,
|
|
918
|
+
prefill: mapped.prefill,
|
|
919
|
+
summary: mapped.summary,
|
|
920
|
+
alternatives: requirement.length,
|
|
921
|
+
requiresCredentials: true,
|
|
922
|
+
...(mapped.cookieHint ? { cookieHint: true } : {}),
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
firstUnsupported ?? (firstUnsupported = {
|
|
926
|
+
declared: true,
|
|
927
|
+
supported: false,
|
|
928
|
+
summary: mapped.summary,
|
|
929
|
+
alternatives: requirement.length,
|
|
930
|
+
requiresCredentials: false,
|
|
931
|
+
unsupportedReason: mapped.unsupportedReason,
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
return (firstUnsupported ?? {
|
|
936
|
+
declared: true,
|
|
937
|
+
supported: false,
|
|
938
|
+
summary: 'unknown security scheme',
|
|
939
|
+
alternatives: requirement.length,
|
|
940
|
+
requiresCredentials: false,
|
|
941
|
+
unsupportedReason: 'The spec declares a security scheme MCPHub cannot recognize.',
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
mapSecurityScheme(schemeName, scheme) {
|
|
945
|
+
switch (scheme.type) {
|
|
946
|
+
case 'http':
|
|
947
|
+
if (scheme.scheme === 'bearer') {
|
|
948
|
+
return {
|
|
949
|
+
ok: true,
|
|
950
|
+
prefill: {
|
|
951
|
+
type: 'http',
|
|
952
|
+
http: {
|
|
953
|
+
scheme: 'bearer',
|
|
954
|
+
...(scheme.bearerFormat ? { bearerFormat: scheme.bearerFormat } : {}),
|
|
955
|
+
},
|
|
956
|
+
},
|
|
957
|
+
summary: scheme.bearerFormat ? `HTTP bearer (${scheme.bearerFormat})` : 'HTTP bearer',
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
if (scheme.scheme === 'basic') {
|
|
961
|
+
return {
|
|
962
|
+
ok: true,
|
|
963
|
+
prefill: { type: 'http', http: { scheme: 'basic' } },
|
|
964
|
+
summary: 'HTTP basic',
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
// MCPHub's form can select digest but applySecurityToClient() never
|
|
968
|
+
// emits a
|
|
969
|
+
// Digest header, so declaring it as prefilled would silently mislead.
|
|
970
|
+
return {
|
|
971
|
+
ok: false,
|
|
972
|
+
summary: `HTTP ${scheme.scheme}`,
|
|
973
|
+
unsupportedReason: `HTTP scheme '${scheme.scheme}' cannot be sent by MCPHub.`,
|
|
974
|
+
};
|
|
975
|
+
case 'apiKey': {
|
|
976
|
+
const location = scheme.in;
|
|
977
|
+
if (location === 'header' || location === 'query' || location === 'cookie') {
|
|
978
|
+
return {
|
|
979
|
+
ok: true,
|
|
980
|
+
prefill: { type: 'apiKey', apiKey: { name: scheme.name, in: location } },
|
|
981
|
+
summary: `API key in ${location} '${scheme.name}'`,
|
|
982
|
+
cookieHint: location === 'cookie',
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
return {
|
|
986
|
+
ok: false,
|
|
987
|
+
summary: `API key in '${location}'`,
|
|
988
|
+
unsupportedReason: `API key location '${location}' is not supported by MCPHub.`,
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
case 'oauth2': {
|
|
992
|
+
// Only flows with a token endpoint are actionable for MCPHub's
|
|
993
|
+
// client-credentials token fetch; prefill it when the spec provides it.
|
|
994
|
+
const tokenUrl = scheme.flows?.clientCredentials?.tokenUrl ||
|
|
995
|
+
scheme.flows?.password?.tokenUrl ||
|
|
996
|
+
scheme.flows?.authorizationCode?.tokenUrl;
|
|
997
|
+
return {
|
|
998
|
+
ok: true,
|
|
999
|
+
prefill: {
|
|
1000
|
+
type: 'oauth2',
|
|
1001
|
+
...(tokenUrl ? { oauth2: { tokenUrl } } : {}),
|
|
1002
|
+
},
|
|
1003
|
+
summary: 'OAuth2',
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
case 'openIdConnect':
|
|
1007
|
+
return {
|
|
1008
|
+
ok: true,
|
|
1009
|
+
prefill: { type: 'openIdConnect', openIdConnect: { url: scheme.openIdConnectUrl } },
|
|
1010
|
+
summary: 'OpenID Connect',
|
|
1011
|
+
};
|
|
1012
|
+
default:
|
|
1013
|
+
return {
|
|
1014
|
+
ok: false,
|
|
1015
|
+
summary: `security scheme '${schemeName}'`,
|
|
1016
|
+
unsupportedReason: `Security scheme type '${scheme.type}' is not supported by MCPHub.`,
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
598
1020
|
isCookieSessionEnabled(sessionId) {
|
|
599
|
-
return !!this.config.openapi?.cookieSession && typeof sessionId === 'string' && sessionId.length > 0;
|
|
1021
|
+
return (!!this.config.openapi?.cookieSession && typeof sessionId === 'string' && sessionId.length > 0);
|
|
600
1022
|
}
|
|
601
1023
|
// Lazily create a per-session cookie jar, seeded with the static apiKey
|
|
602
1024
|
// cookie (if any) so it applies to every request and can be overridden by a
|