@samanhappy/mcphub 1.0.32 → 1.0.34
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 +12 -6
- package/README.md +23 -7
- package/README.zh.md +24 -8
- package/dist/clients/openapi.js +439 -72
- package/dist/clients/openapi.js.map +1 -1
- package/dist/config/index.js +24 -2
- package/dist/config/index.js.map +1 -1
- package/dist/constants/oauthServerDefaults.js +5 -0
- package/dist/constants/oauthServerDefaults.js.map +1 -1
- package/dist/controllers/activityController.js +11 -0
- package/dist/controllers/activityController.js.map +1 -1
- package/dist/controllers/logController.js +10 -3
- package/dist/controllers/logController.js.map +1 -1
- package/dist/controllers/mcpbController.js +6 -13
- package/dist/controllers/mcpbController.js.map +1 -1
- package/dist/controllers/oauthCallbackController.js +26 -0
- package/dist/controllers/oauthCallbackController.js.map +1 -1
- package/dist/controllers/oauthServerController.js +40 -4
- package/dist/controllers/oauthServerController.js.map +1 -1
- package/dist/controllers/serverController.js +122 -31
- package/dist/controllers/serverController.js.map +1 -1
- package/dist/controllers/templateController.js +1 -1
- package/dist/controllers/templateController.js.map +1 -1
- package/dist/routes/index.js +2 -1
- package/dist/routes/index.js.map +1 -1
- package/dist/services/authorizationService.js +28 -0
- package/dist/services/authorizationService.js.map +1 -0
- package/dist/services/cimdClientService.js +195 -0
- package/dist/services/cimdClientService.js.map +1 -0
- package/dist/services/mcpOAuthProvider.js +2 -2
- package/dist/services/mcpOAuthProvider.js.map +1 -1
- package/dist/services/mcpService.js +108 -11
- package/dist/services/mcpService.js.map +1 -1
- package/dist/services/oauthClientRegistration.js +27 -9
- package/dist/services/oauthClientRegistration.js.map +1 -1
- package/dist/services/oauthServerService.js +17 -1
- package/dist/services/oauthServerService.js.map +1 -1
- package/dist/services/openApiToolStatsService.js +37 -0
- package/dist/services/openApiToolStatsService.js.map +1 -0
- package/dist/services/serverConfigPresenter.js +85 -0
- package/dist/services/serverConfigPresenter.js.map +1 -0
- package/dist/services/templateService.js +48 -11
- package/dist/services/templateService.js.map +1 -1
- package/dist/services/vectorSearchService.js +31 -6
- package/dist/services/vectorSearchService.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/processTree.js +47 -0
- package/dist/utils/processTree.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/smartRouting.js +9 -4
- package/dist/utils/smartRouting.js.map +1 -1
- package/dist/utils/ssrf.js +6 -5
- package/dist/utils/ssrf.js.map +1 -1
- package/dist/utils/systemConfigCache.js +40 -2
- package/dist/utils/systemConfigCache.js.map +1 -1
- package/frontend/dist/assets/{ActivityPage-C6m1hGLq.js → ActivityPage-BYoyi-IG.js} +2 -2
- package/frontend/dist/assets/{ActivityPage-C6m1hGLq.js.map → ActivityPage-BYoyi-IG.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-BOHkLPxE.js +2 -0
- package/frontend/dist/assets/{Dashboard-DFKfjPff.js.map → Dashboard-BOHkLPxE.js.map} +1 -1
- package/frontend/dist/assets/{EndpointCopy-CP_Fb1To.js → EndpointCopy-DDVLhiUy.js} +2 -2
- package/frontend/dist/assets/{EndpointCopy-CP_Fb1To.js.map → EndpointCopy-DDVLhiUy.js.map} +1 -1
- package/frontend/dist/assets/GroupsPage-B3bHSWmT.js +33 -0
- package/frontend/dist/assets/{GroupsPage-Cwfm5q-a.js.map → GroupsPage-B3bHSWmT.js.map} +1 -1
- package/frontend/dist/assets/{LoginPage-CnF44h_n.js → LoginPage-CfAnFIbQ.js} +2 -2
- package/frontend/dist/assets/{LoginPage-CnF44h_n.js.map → LoginPage-CfAnFIbQ.js.map} +1 -1
- package/frontend/dist/assets/{LogsPage-BFIpICsM.js → LogsPage-DT2VotXH.js} +2 -2
- package/frontend/dist/assets/{LogsPage-BFIpICsM.js.map → LogsPage-DT2VotXH.js.map} +1 -1
- package/frontend/dist/assets/MarketPage-Cn0J7UNX.js +3 -0
- package/frontend/dist/assets/MarketPage-Cn0J7UNX.js.map +1 -0
- package/frontend/dist/assets/{OAuthConsentPage-zqXUvKMF.js → OAuthConsentPage-DnKLaKDH.js} +2 -2
- package/frontend/dist/assets/{OAuthConsentPage-zqXUvKMF.js.map → OAuthConsentPage-DnKLaKDH.js.map} +1 -1
- package/frontend/dist/assets/{PromptsPage-Cm8ykiI4.js → PromptsPage-Ccg3fJWt.js} +2 -2
- package/frontend/dist/assets/{PromptsPage-Cm8ykiI4.js.map → PromptsPage-Ccg3fJWt.js.map} +1 -1
- package/frontend/dist/assets/{ResourcesPage-UMyO6mQ4.js → ResourcesPage-V45CxfvA.js} +2 -2
- package/frontend/dist/assets/{ResourcesPage-UMyO6mQ4.js.map → ResourcesPage-V45CxfvA.js.map} +1 -1
- package/frontend/dist/assets/{ServersPage-B0RoM8ez.js → ServersPage-DA_nkhLB.js} +5 -5
- package/frontend/dist/assets/{ServersPage-B0RoM8ez.js.map → ServersPage-DA_nkhLB.js.map} +1 -1
- package/frontend/dist/assets/SettingsPage-BJjO5cte.js +12 -0
- package/frontend/dist/assets/SettingsPage-BJjO5cte.js.map +1 -0
- package/frontend/dist/assets/{StatusDot-BAMXeT2x.js → StatusDot-DcjrhMNI.js} +2 -2
- package/frontend/dist/assets/{StatusDot-BAMXeT2x.js.map → StatusDot-DcjrhMNI.js.map} +1 -1
- package/frontend/dist/assets/{ToggleGroup-Cfgt5lJo.js → ToggleGroup-BSLtGF--.js} +2 -2
- package/frontend/dist/assets/{ToggleGroup-Cfgt5lJo.js.map → ToggleGroup-BSLtGF--.js.map} +1 -1
- package/frontend/dist/assets/UsersPage-DKdCe4GT.js +2 -0
- package/frontend/dist/assets/UsersPage-DKdCe4GT.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/index-DjExFs33.js +3 -0
- package/frontend/dist/assets/index-DjExFs33.js.map +1 -0
- 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-D2QmlFby.js → resourceService-DIUK5vbC.js} +2 -2
- package/frontend/dist/assets/{resourceService-D2QmlFby.js.map → resourceService-DIUK5vbC.js.map} +1 -1
- package/frontend/dist/assets/useCostData-CSeCIQmM.js +2 -0
- package/frontend/dist/assets/useCostData-CSeCIQmM.js.map +1 -0
- package/frontend/dist/assets/useSettingsData-CvAOhPaX.js +2 -0
- package/frontend/dist/assets/{useSettingsData-CeCvZ2nU.js.map → useSettingsData-CvAOhPaX.js.map} +1 -1
- package/frontend/dist/assets/variableDetection-BeF9gp9K.js +16 -0
- package/frontend/dist/assets/variableDetection-BeF9gp9K.js.map +1 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +7 -7
- package/frontend/dist/assets/ConfirmDialog-BCMNzi21.js +0 -2
- package/frontend/dist/assets/ConfirmDialog-BCMNzi21.js.map +0 -1
- package/frontend/dist/assets/Dashboard-DFKfjPff.js +0 -2
- package/frontend/dist/assets/GroupsPage-Cwfm5q-a.js +0 -33
- package/frontend/dist/assets/MarketPage-Du-NqiEl.js +0 -3
- package/frontend/dist/assets/MarketPage-Du-NqiEl.js.map +0 -1
- package/frontend/dist/assets/SettingsPage-BTB-36-n.js +0 -12
- package/frontend/dist/assets/SettingsPage-BTB-36-n.js.map +0 -1
- package/frontend/dist/assets/UsersPage-DlzYf7Su.js +0 -2
- package/frontend/dist/assets/UsersPage-DlzYf7Su.js.map +0 -1
- package/frontend/dist/assets/contextCost-kp2vZYp7.js +0 -2
- package/frontend/dist/assets/contextCost-kp2vZYp7.js.map +0 -1
- package/frontend/dist/assets/index-CGvImZ3F.css +0 -1
- package/frontend/dist/assets/index-CvXw771N.js +0 -3
- package/frontend/dist/assets/index-CvXw771N.js.map +0 -1
- package/frontend/dist/assets/useSettingsData-CeCvZ2nU.js +0 -2
- package/frontend/dist/assets/variableDetection-DKPiHkxr.js +0 -16
- package/frontend/dist/assets/variableDetection-DKPiHkxr.js.map +0 -1
package/dist/clients/openapi.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { readFile } from 'node:fs/promises';
|
|
3
4
|
import { CookieJar } from 'tough-cookie';
|
|
4
5
|
import SwaggerParser from '@apidevtools/swagger-parser';
|
|
@@ -6,6 +7,51 @@ import * as yaml from 'js-yaml';
|
|
|
6
7
|
import { assertSafeUrl, UnsafeUrlError, createRedirectValidatingFetch } from '../utils/ssrf.js';
|
|
7
8
|
import { getUserDao } from '../dao/index.js';
|
|
8
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
|
+
}
|
|
9
55
|
export class OpenAPIClient {
|
|
10
56
|
constructor(config, options = {}) {
|
|
11
57
|
this.config = config;
|
|
@@ -21,20 +67,47 @@ export class OpenAPIClient {
|
|
|
21
67
|
if (!config.openapi?.url && !config.openapi?.schema) {
|
|
22
68
|
throw new Error('OpenAPI URL or schema is required');
|
|
23
69
|
}
|
|
24
|
-
// 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.
|
|
25
76
|
this.baseUrl = config.openapi?.url ? this.extractBaseUrl(config.openapi.url) : '';
|
|
26
77
|
this.securityConfig = config.openapi.security;
|
|
27
78
|
this.persistOAuth2Token = options.persistOAuth2Token;
|
|
28
|
-
this.httpClient =
|
|
29
|
-
|
|
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({
|
|
30
101
|
timeout: config.options?.timeout || 30000,
|
|
31
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 },
|
|
32
106
|
headers: {
|
|
33
107
|
'Content-Type': 'application/json',
|
|
34
108
|
...config.headers,
|
|
35
109
|
},
|
|
36
110
|
});
|
|
37
|
-
this.setupSecurity();
|
|
38
111
|
}
|
|
39
112
|
extractBaseUrl(specUrl) {
|
|
40
113
|
try {
|
|
@@ -46,57 +119,68 @@ export class OpenAPIClient {
|
|
|
46
119
|
return '';
|
|
47
120
|
}
|
|
48
121
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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 {};
|
|
52
130
|
}
|
|
53
|
-
switch (
|
|
54
|
-
case 'apiKey':
|
|
55
|
-
if (
|
|
56
|
-
const { name, in: location, value } =
|
|
57
|
-
|
|
58
|
-
|
|
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;
|
|
59
140
|
}
|
|
60
|
-
else if (location === 'query') {
|
|
61
|
-
|
|
141
|
+
else if (location === 'query' && value) {
|
|
142
|
+
client.interceptors.request.use((config) => {
|
|
62
143
|
config.params = { ...config.params, [name]: value };
|
|
63
144
|
return config;
|
|
64
145
|
});
|
|
65
146
|
}
|
|
66
|
-
else if (location === 'cookie') {
|
|
67
|
-
|
|
147
|
+
else if (location === 'cookie' && value) {
|
|
148
|
+
return { staticCookieHeader: `${name}=${value}` };
|
|
68
149
|
}
|
|
69
150
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
151
|
+
return {};
|
|
152
|
+
}
|
|
153
|
+
case 'http': {
|
|
154
|
+
if (securityConfig.http) {
|
|
155
|
+
const { scheme, credentials } = securityConfig.http;
|
|
74
156
|
if (scheme === 'bearer' && credentials) {
|
|
75
|
-
|
|
157
|
+
client.defaults.headers.common['Authorization'] = `Bearer ${credentials}`;
|
|
76
158
|
}
|
|
77
159
|
else if (scheme === 'basic' && credentials) {
|
|
78
|
-
|
|
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
|
+
}
|
|
79
166
|
}
|
|
80
167
|
}
|
|
81
|
-
|
|
168
|
+
return {};
|
|
169
|
+
}
|
|
82
170
|
case 'oauth2':
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
break;
|
|
171
|
+
this.setAuthorizationHeader(client, securityConfig.oauth2?.token);
|
|
172
|
+
return {};
|
|
87
173
|
case 'openIdConnect':
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
break;
|
|
174
|
+
this.setAuthorizationHeader(client, securityConfig.openIdConnect?.token);
|
|
175
|
+
return {};
|
|
92
176
|
}
|
|
93
177
|
}
|
|
94
|
-
setAuthorizationHeader(token) {
|
|
178
|
+
setAuthorizationHeader(client, token) {
|
|
95
179
|
if (token) {
|
|
96
|
-
|
|
180
|
+
client.defaults.headers.common['Authorization'] = 'Bearer ' + token;
|
|
97
181
|
return;
|
|
98
182
|
}
|
|
99
|
-
delete
|
|
183
|
+
delete client.defaults.headers.common['Authorization'];
|
|
100
184
|
}
|
|
101
185
|
getOAuth2Config() {
|
|
102
186
|
return this.securityConfig?.type === 'oauth2' ? this.securityConfig.oauth2 : undefined;
|
|
@@ -115,7 +199,7 @@ export class OpenAPIClient {
|
|
|
115
199
|
if (this.config.openapi?.security?.oauth2) {
|
|
116
200
|
this.config.openapi.security.oauth2 = oauth2;
|
|
117
201
|
}
|
|
118
|
-
this.setAuthorizationHeader(undefined);
|
|
202
|
+
this.setAuthorizationHeader(this.httpClient, undefined);
|
|
119
203
|
await this.persistOAuth2Token?.({ ...oauth2 });
|
|
120
204
|
return true;
|
|
121
205
|
}
|
|
@@ -143,7 +227,7 @@ export class OpenAPIClient {
|
|
|
143
227
|
if (this.config.openapi?.security?.oauth2) {
|
|
144
228
|
this.config.openapi.security.oauth2 = oauth2;
|
|
145
229
|
}
|
|
146
|
-
this.setAuthorizationHeader(token);
|
|
230
|
+
this.setAuthorizationHeader(this.httpClient, token);
|
|
147
231
|
await this.persistOAuth2Token?.(oauth2);
|
|
148
232
|
}
|
|
149
233
|
async fetchOAuth2ClientCredentialsToken(oauth2) {
|
|
@@ -160,9 +244,15 @@ export class OpenAPIClient {
|
|
|
160
244
|
if (Array.isArray(oauth2.scopes) && oauth2.scopes.length > 0) {
|
|
161
245
|
body.set('scope', oauth2.scopes.join(' '));
|
|
162
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
|
+
});
|
|
163
253
|
const response = await this.httpClient.request({
|
|
164
254
|
method: 'post',
|
|
165
|
-
url:
|
|
255
|
+
url: safeTokenUrl,
|
|
166
256
|
baseURL: undefined,
|
|
167
257
|
headers: {
|
|
168
258
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -185,12 +275,12 @@ export class OpenAPIClient {
|
|
|
185
275
|
return undefined;
|
|
186
276
|
}
|
|
187
277
|
if (this.hasValidOAuth2Token(oauth2)) {
|
|
188
|
-
this.setAuthorizationHeader(oauth2.token);
|
|
278
|
+
this.setAuthorizationHeader(this.httpClient, oauth2.token);
|
|
189
279
|
return oauth2.token;
|
|
190
280
|
}
|
|
191
281
|
if (!oauth2.tokenUrl || !oauth2.clientId) {
|
|
192
282
|
if (oauth2.token) {
|
|
193
|
-
this.setAuthorizationHeader(oauth2.token);
|
|
283
|
+
this.setAuthorizationHeader(this.httpClient, oauth2.token);
|
|
194
284
|
}
|
|
195
285
|
return oauth2.token;
|
|
196
286
|
}
|
|
@@ -220,27 +310,37 @@ export class OpenAPIClient {
|
|
|
220
310
|
this.refGuardRejection = undefined;
|
|
221
311
|
// Parse and dereference the OpenAPI specification
|
|
222
312
|
if (this.config.openapi?.url) {
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
// the main document so its (unauthenticated) headers never see the
|
|
228
|
-
// credentials; external $ref resolution still uses that resolver and
|
|
229
|
-
// 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.
|
|
230
317
|
const specUrl = this.config.openapi.url;
|
|
231
|
-
await assertSafeUrl(specUrl, {
|
|
318
|
+
const safeSpecUrl = await assertSafeUrl(specUrl, {
|
|
319
|
+
allowInternal: this.allowInternalNetworks,
|
|
320
|
+
});
|
|
232
321
|
const requestConfig = {
|
|
233
322
|
responseType: 'text',
|
|
234
323
|
transformResponse: [(data) => data],
|
|
235
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;
|
|
236
336
|
// The static apiKey.in:'cookie' value is otherwise only injected in
|
|
237
|
-
// callTool; apply it here
|
|
238
|
-
if (
|
|
239
|
-
requestConfig.headers = { Cookie:
|
|
337
|
+
// callTool; apply it here so cookie-protected spec URLs load.
|
|
338
|
+
if (specCookie) {
|
|
339
|
+
requestConfig.headers = { Cookie: specCookie };
|
|
240
340
|
}
|
|
241
|
-
const response = await
|
|
341
|
+
const response = await specClient.get(safeSpecUrl, requestConfig);
|
|
242
342
|
const raw = typeof response.data === 'string' ? response.data : String(response.data);
|
|
243
|
-
this.spec = (await SwaggerParser.dereference(
|
|
343
|
+
this.spec = (await SwaggerParser.dereference(safeSpecUrl, this.parseSpecDocument(raw), this.guardedRefResolveOptions()));
|
|
244
344
|
}
|
|
245
345
|
else if (this.config.openapi?.schema) {
|
|
246
346
|
// For schema object, we need to pass it as a cloned object
|
|
@@ -298,18 +398,22 @@ export class OpenAPIClient {
|
|
|
298
398
|
order: 1,
|
|
299
399
|
canRead: (file) => /^https?:\/\//i.test(file.url),
|
|
300
400
|
read: async (file) => {
|
|
401
|
+
let safeRefUrl;
|
|
301
402
|
try {
|
|
302
|
-
await assertSafeUrl(file.url, {
|
|
403
|
+
safeRefUrl = await assertSafeUrl(file.url, {
|
|
404
|
+
allowInternal: this.allowInternalNetworks,
|
|
405
|
+
});
|
|
303
406
|
}
|
|
304
407
|
catch (error) {
|
|
305
408
|
rejectUnsafe(error);
|
|
409
|
+
return '';
|
|
306
410
|
}
|
|
307
411
|
// Plain fetch, NOT this.httpClient: default auth headers and
|
|
308
412
|
// interceptors must never be forwarded cross-origin to $ref
|
|
309
413
|
// targets (#1044). Redirects are followed manually with every hop
|
|
310
414
|
// re-validated by the guard.
|
|
311
415
|
const safeFetch = createRedirectValidatingFetch((url, init) => fetch(url, init), this.allowInternalNetworks);
|
|
312
|
-
const response = await safeFetch(
|
|
416
|
+
const response = await safeFetch(safeRefUrl, { method: 'GET', headers: {} });
|
|
313
417
|
const buf = await response.arrayBuffer();
|
|
314
418
|
return new TextDecoder().decode(buf);
|
|
315
419
|
},
|
|
@@ -378,8 +482,6 @@ export class OpenAPIClient {
|
|
|
378
482
|
this.baseUrl = `${originalUrl.protocol}//${originalUrl.host}/${serverUrl}`;
|
|
379
483
|
}
|
|
380
484
|
}
|
|
381
|
-
// Update HTTP client's baseURL
|
|
382
|
-
this.httpClient.defaults.baseURL = this.baseUrl;
|
|
383
485
|
}
|
|
384
486
|
extractTools() {
|
|
385
487
|
if (!this.spec?.paths) {
|
|
@@ -421,15 +523,30 @@ export class OpenAPIClient {
|
|
|
421
523
|
operationName = uniqueName;
|
|
422
524
|
}
|
|
423
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
|
+
}
|
|
424
541
|
const tool = {
|
|
425
542
|
name: operationName,
|
|
426
|
-
description
|
|
543
|
+
description,
|
|
427
544
|
// SwaggerParser.dereference turns recursive $ref schemas into live
|
|
428
545
|
// circular references on the dereferenced spec objects. generateInputSchema
|
|
429
546
|
// references those objects directly, so without sanitization every
|
|
430
547
|
// downstream serializer (tokenCost, getServerConfig, MCP ListTools,
|
|
431
548
|
// embeddings) throws "Converting circular structure to JSON". See #959.
|
|
432
|
-
inputSchema: createSafeJSON(this.generateInputSchema(operation, path, method)),
|
|
549
|
+
inputSchema: createSafeJSON(this.generateInputSchema(operation, path, method, requestBodySelection)),
|
|
433
550
|
operationId: operation.operationId || operationName,
|
|
434
551
|
method: method,
|
|
435
552
|
path,
|
|
@@ -441,7 +558,7 @@ export class OpenAPIClient {
|
|
|
441
558
|
}
|
|
442
559
|
}
|
|
443
560
|
}
|
|
444
|
-
generateInputSchema(operation, _path, _method) {
|
|
561
|
+
generateInputSchema(operation, _path, _method, requestBodySelection) {
|
|
445
562
|
const schema = {
|
|
446
563
|
type: 'object',
|
|
447
564
|
properties: {},
|
|
@@ -482,12 +599,18 @@ export class OpenAPIClient {
|
|
|
482
599
|
}
|
|
483
600
|
}
|
|
484
601
|
}
|
|
485
|
-
// Handle request body
|
|
486
|
-
|
|
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) {
|
|
487
607
|
const requestBody = operation.requestBody;
|
|
488
|
-
const
|
|
489
|
-
if (
|
|
490
|
-
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;
|
|
491
614
|
if (requestBody.required) {
|
|
492
615
|
required.push('body');
|
|
493
616
|
}
|
|
@@ -523,9 +646,14 @@ export class OpenAPIClient {
|
|
|
523
646
|
const pathParams = tool.parameters?.filter((p) => p.in === 'path') || [];
|
|
524
647
|
for (const param of pathParams) {
|
|
525
648
|
const value = args[param.name];
|
|
526
|
-
if (value
|
|
527
|
-
|
|
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`);
|
|
528
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));
|
|
529
657
|
}
|
|
530
658
|
// Build query parameters
|
|
531
659
|
const queryParams = {};
|
|
@@ -542,9 +670,41 @@ export class OpenAPIClient {
|
|
|
542
670
|
url,
|
|
543
671
|
params: queryParams,
|
|
544
672
|
};
|
|
545
|
-
// Add request body if applicable
|
|
546
|
-
|
|
547
|
-
|
|
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
|
+
}
|
|
548
708
|
}
|
|
549
709
|
// Collect all headers to be sent
|
|
550
710
|
const allHeaders = {};
|
|
@@ -564,6 +724,11 @@ export class OpenAPIClient {
|
|
|
564
724
|
}
|
|
565
725
|
}
|
|
566
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
|
+
}
|
|
567
732
|
// Set headers if any were collected
|
|
568
733
|
if (Object.keys(allHeaders).length > 0) {
|
|
569
734
|
requestConfig.headers = allHeaders;
|
|
@@ -574,15 +739,34 @@ export class OpenAPIClient {
|
|
|
574
739
|
// final URL rather than trusting either alone.
|
|
575
740
|
resolvedTarget = null;
|
|
576
741
|
try {
|
|
577
|
-
|
|
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);
|
|
578
759
|
}
|
|
579
760
|
catch {
|
|
580
761
|
// relative path with no base — no host to validate; axios surfaces the error
|
|
581
762
|
}
|
|
582
763
|
if (resolvedTarget) {
|
|
583
|
-
await assertSafeUrl(resolvedTarget.href, {
|
|
764
|
+
const safeTargetUrl = await assertSafeUrl(resolvedTarget.href, {
|
|
584
765
|
allowInternal: this.allowInternalNetworks,
|
|
585
766
|
});
|
|
767
|
+
const safeTarget = new URL(safeTargetUrl);
|
|
768
|
+
requestConfig.baseURL = safeTarget.origin;
|
|
769
|
+
requestConfig.url = `${safeTarget.pathname}${safeTarget.search}`;
|
|
586
770
|
}
|
|
587
771
|
const cookieSessionEnabled = this.isCookieSessionEnabled(sessionId);
|
|
588
772
|
// Inject cookies when either the dynamic session store is active for this
|
|
@@ -650,6 +834,189 @@ export class OpenAPIClient {
|
|
|
650
834
|
getSpec() {
|
|
651
835
|
return this.spec;
|
|
652
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
|
+
}
|
|
653
1020
|
isCookieSessionEnabled(sessionId) {
|
|
654
1021
|
return (!!this.config.openapi?.cookieSession && typeof sessionId === 'string' && sessionId.length > 0);
|
|
655
1022
|
}
|