@mcp-native/mcp 0.5.0 → 0.6.0
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 +172 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +98 -11
- package/dist/index.js.map +1 -1
- package/dist/lifecycle.d.ts +103 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +425 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/oauth-error.d.ts +6 -0
- package/dist/oauth-error.d.ts.map +1 -0
- package/dist/oauth-error.js +9 -0
- package/dist/oauth-error.js.map +1 -0
- package/dist/oauth-native.d.ts +66 -0
- package/dist/oauth-native.d.ts.map +1 -0
- package/dist/oauth-native.js +499 -0
- package/dist/oauth-native.js.map +1 -0
- package/dist/oauth-url.d.ts +9 -0
- package/dist/oauth-url.d.ts.map +1 -0
- package/dist/oauth-url.js +38 -0
- package/dist/oauth-url.js.map +1 -0
- package/dist/oauth.d.ts +147 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +1183 -0
- package/dist/oauth.js.map +1 -0
- package/package.json +7 -2
package/dist/oauth.js
ADDED
|
@@ -0,0 +1,1183 @@
|
|
|
1
|
+
import { checkResourceAllowed, resolveClientMetadata, resourceUrlFromServerUrl, StreamableHTTPClientTransport, validateClientMetadataUrl, } from "@modelcontextprotocol/client";
|
|
2
|
+
import { OAuthClientInformationFullSchema, OAuthClientInformationSchema, OAuthClientMetadataSchema, OAuthMetadataSchema, OAuthProtectedResourceMetadataSchema, OAuthTokensSchema, OpenIdProviderDiscoveryMetadataSchema, } from "@modelcontextprotocol/core";
|
|
3
|
+
import { McpNativeOAuthError } from "./oauth-error.js";
|
|
4
|
+
import { assertOAuthRedirectParameterBudget, isLoopbackHostname, MAX_AUTHORIZATION_URL_CODE_UNITS, MAX_CALLBACK_CODE_UNITS, MAX_CALLBACK_PARAMETERS, MAX_CALLBACK_PARAMETER_NAME_CODE_UNITS, MAX_CALLBACK_PARAMETER_VALUE_CODE_UNITS, MAX_OAUTH_ISSUER_CODE_UNITS, } from "./oauth-url.js";
|
|
5
|
+
export { McpNativeOAuthError } from "./oauth-error.js";
|
|
6
|
+
export { McpNativeOAuthAuthorizationSession, McpNativeOAuthPlatformSecureStore, createMcpNativeOAuthAuthorizationSession, createMcpNativeOAuthPlatformSecureStore, } from "./oauth-native.js";
|
|
7
|
+
const CALLBACK_PARAMETER_NAMES = new Set([
|
|
8
|
+
"code",
|
|
9
|
+
"error",
|
|
10
|
+
"error_description",
|
|
11
|
+
"error_uri",
|
|
12
|
+
"iss",
|
|
13
|
+
"state",
|
|
14
|
+
]);
|
|
15
|
+
const STATE_PATTERN = /^[A-Za-z0-9._~-]{32,512}$/u;
|
|
16
|
+
const PKCE_VERIFIER_PATTERN = /^[A-Za-z0-9._~-]{43,128}$/u;
|
|
17
|
+
const SCOPE_TOKEN_PATTERN = /^[\u0021\u0023-\u005b\u005d-\u007e]{1,256}$/u;
|
|
18
|
+
const PRIVATE_USE_REDIRECT_SCHEME_PATTERN = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+:$/u;
|
|
19
|
+
const RESERVED_REDIRECT_SCHEMES = new Set([
|
|
20
|
+
"about:",
|
|
21
|
+
"blob:",
|
|
22
|
+
"chrome-extension:",
|
|
23
|
+
"chrome:",
|
|
24
|
+
"content:",
|
|
25
|
+
"data:",
|
|
26
|
+
"file:",
|
|
27
|
+
"ftp:",
|
|
28
|
+
"http:",
|
|
29
|
+
"https:",
|
|
30
|
+
"intent:",
|
|
31
|
+
"javascript:",
|
|
32
|
+
"mailto:",
|
|
33
|
+
"tel:",
|
|
34
|
+
"vbscript:",
|
|
35
|
+
"ws:",
|
|
36
|
+
"wss:",
|
|
37
|
+
]);
|
|
38
|
+
const MAX_SCOPE_CODE_UNITS = 2_048;
|
|
39
|
+
const MAX_SCOPE_TOKENS = 64;
|
|
40
|
+
const MAX_RESOURCE_IDENTIFIER_CODE_UNITS = 4_096;
|
|
41
|
+
const CALLBACK_VALUE_LIMITS = Object.freeze({
|
|
42
|
+
code: 4_096,
|
|
43
|
+
error: 256,
|
|
44
|
+
error_description: 2_048,
|
|
45
|
+
error_uri: 2_048,
|
|
46
|
+
iss: 2_048,
|
|
47
|
+
state: 512,
|
|
48
|
+
});
|
|
49
|
+
const MAX_TOKEN_VALUE_CODE_UNITS = 16_384;
|
|
50
|
+
const MAX_TOKEN_TYPE_CODE_UNITS = 64;
|
|
51
|
+
const MAX_TOKEN_CUMULATIVE_CODE_UNITS = 24_576;
|
|
52
|
+
const TOKEN_STRING_LIMITS = Object.freeze({
|
|
53
|
+
access_token: MAX_TOKEN_VALUE_CODE_UNITS,
|
|
54
|
+
id_token: MAX_TOKEN_VALUE_CODE_UNITS,
|
|
55
|
+
issuer: 2_048,
|
|
56
|
+
refresh_token: MAX_TOKEN_VALUE_CODE_UNITS,
|
|
57
|
+
scope: MAX_SCOPE_CODE_UNITS,
|
|
58
|
+
token_type: MAX_TOKEN_TYPE_CODE_UNITS,
|
|
59
|
+
});
|
|
60
|
+
const TOKEN_INFORMATION_BUDGET = Object.freeze({
|
|
61
|
+
maxArrayItems: 64,
|
|
62
|
+
maxCumulativeArrayItems: 128,
|
|
63
|
+
maxCumulativeProperties: 128,
|
|
64
|
+
maxDepth: 8,
|
|
65
|
+
maxNodes: 256,
|
|
66
|
+
maxObjectProperties: 64,
|
|
67
|
+
maxPropertyNameCodeUnits: 128,
|
|
68
|
+
maxStringCodeUnits: MAX_TOKEN_VALUE_CODE_UNITS,
|
|
69
|
+
maxTotalStringCodeUnits: MAX_TOKEN_CUMULATIVE_CODE_UNITS,
|
|
70
|
+
});
|
|
71
|
+
const MAX_CLIENT_IDENTIFIER_CODE_UNITS = 4_096;
|
|
72
|
+
const MAX_CLIENT_SECRET_CODE_UNITS = 4_096;
|
|
73
|
+
const CLIENT_INFORMATION_BUDGET = Object.freeze({
|
|
74
|
+
maxArrayItems: 64,
|
|
75
|
+
maxCumulativeArrayItems: 256,
|
|
76
|
+
maxCumulativeProperties: 256,
|
|
77
|
+
maxDepth: 8,
|
|
78
|
+
maxNodes: 512,
|
|
79
|
+
maxObjectProperties: 64,
|
|
80
|
+
maxPropertyNameCodeUnits: 128,
|
|
81
|
+
maxStringCodeUnits: 8_192,
|
|
82
|
+
maxTotalStringCodeUnits: 24_576,
|
|
83
|
+
});
|
|
84
|
+
const DISCOVERY_STATE_BUDGET = Object.freeze({
|
|
85
|
+
allowedUndefinedRootProperties: new Set([
|
|
86
|
+
"authorizationServerMetadata",
|
|
87
|
+
"resourceMetadata",
|
|
88
|
+
"resourceMetadataUrl",
|
|
89
|
+
]),
|
|
90
|
+
maxArrayItems: 64,
|
|
91
|
+
maxCumulativeArrayItems: 256,
|
|
92
|
+
maxCumulativeProperties: 256,
|
|
93
|
+
maxDepth: 8,
|
|
94
|
+
maxNodes: 512,
|
|
95
|
+
maxObjectProperties: 128,
|
|
96
|
+
maxPropertyNameCodeUnits: 128,
|
|
97
|
+
maxStringCodeUnits: 4_096,
|
|
98
|
+
maxTotalStringCodeUnits: 24_576,
|
|
99
|
+
});
|
|
100
|
+
/**
|
|
101
|
+
* SDK v2 OAuth provider with an explicit native-host persistence and callback boundary.
|
|
102
|
+
* Discovery, PKCE, issuer validation, token exchange, and refresh remain owned by the
|
|
103
|
+
* official SDK; this class pins their host-controlled seams.
|
|
104
|
+
*/
|
|
105
|
+
export class McpNativeOAuthClientProvider {
|
|
106
|
+
redirectUrl;
|
|
107
|
+
clientMetadata;
|
|
108
|
+
clientMetadataUrl;
|
|
109
|
+
#resourceUrl;
|
|
110
|
+
#storage;
|
|
111
|
+
#scopeStore;
|
|
112
|
+
#createState;
|
|
113
|
+
#openAuthorization;
|
|
114
|
+
#approveReauthorization;
|
|
115
|
+
#authorizationOwner = Object.freeze({});
|
|
116
|
+
#activeIssuer;
|
|
117
|
+
#pendingAuthorizationUrl;
|
|
118
|
+
#authorizationAttemptReserved = false;
|
|
119
|
+
#authorizationCleanupRunning = false;
|
|
120
|
+
#authorizationCodeVerifierSaved = false;
|
|
121
|
+
#authorizationCodeVerifierSetupRunning = false;
|
|
122
|
+
#authorizationStateSetupRunning = false;
|
|
123
|
+
#authorizationCompletionRunning = false;
|
|
124
|
+
#authorizationHandoffRunning = false;
|
|
125
|
+
constructor(options) {
|
|
126
|
+
assertSecureStore(options.storage);
|
|
127
|
+
assertScopeStore(options.scopeStore);
|
|
128
|
+
if (typeof options.createState !== "function" ||
|
|
129
|
+
typeof options.openAuthorization !== "function" ||
|
|
130
|
+
(options.approveReauthorization !== undefined &&
|
|
131
|
+
typeof options.approveReauthorization !== "function")) {
|
|
132
|
+
throw new McpNativeOAuthError("invalid-configuration", "OAuth provider callbacks must be functions");
|
|
133
|
+
}
|
|
134
|
+
this.#resourceUrl = resourceUrlFromServerUrl(parseProtectedServerUrl(options.serverUrl));
|
|
135
|
+
this.redirectUrl = parseRedirectUrl(options.redirectUrl);
|
|
136
|
+
validateClientMetadataUrl(options.clientMetadataUrl);
|
|
137
|
+
if (options.clientMetadataUrl !== undefined) {
|
|
138
|
+
this.clientMetadataUrl = options.clientMetadataUrl;
|
|
139
|
+
}
|
|
140
|
+
const metadataResult = OAuthClientMetadataSchema.safeParse(options.clientMetadata);
|
|
141
|
+
if (!metadataResult.success) {
|
|
142
|
+
throw new McpNativeOAuthError("invalid-configuration", "OAuth client metadata does not match the official SDK schema", { cause: metadataResult.error });
|
|
143
|
+
}
|
|
144
|
+
for (const redirectUrl of metadataResult.data.redirect_uris) {
|
|
145
|
+
parseRedirectUrl(redirectUrl);
|
|
146
|
+
}
|
|
147
|
+
if (!metadataResult.data.redirect_uris.includes(this.redirectUrl.href)) {
|
|
148
|
+
throw new McpNativeOAuthError("invalid-configuration", "OAuth client metadata must contain the exact configured redirect URL");
|
|
149
|
+
}
|
|
150
|
+
this.clientMetadata = Object.freeze({
|
|
151
|
+
...resolveClientMetadata({
|
|
152
|
+
clientMetadata: metadataResult.data,
|
|
153
|
+
redirectUrl: this.redirectUrl,
|
|
154
|
+
}),
|
|
155
|
+
application_type: metadataResult.data.application_type ?? "native",
|
|
156
|
+
});
|
|
157
|
+
this.#storage = options.storage;
|
|
158
|
+
this.#scopeStore = options.scopeStore;
|
|
159
|
+
this.#createState = options.createState;
|
|
160
|
+
this.#openAuthorization = options.openAuthorization;
|
|
161
|
+
this.#approveReauthorization = options.approveReauthorization;
|
|
162
|
+
}
|
|
163
|
+
async state() {
|
|
164
|
+
if (this.#authorizationAttemptReserved || this.#authorizationCleanupRunning) {
|
|
165
|
+
throw new McpNativeOAuthError("invalid-configuration", "Another OAuth authorization attempt is already pending");
|
|
166
|
+
}
|
|
167
|
+
this.#authorizationAttemptReserved = true;
|
|
168
|
+
this.#authorizationCodeVerifierSaved = false;
|
|
169
|
+
this.#authorizationStateSetupRunning = true;
|
|
170
|
+
let storageReserved = false;
|
|
171
|
+
try {
|
|
172
|
+
await this.#storage.reserveOAuthState(this.#authorizationOwner);
|
|
173
|
+
storageReserved = true;
|
|
174
|
+
const state = await this.#createState();
|
|
175
|
+
if (typeof state !== "string" || !STATE_PATTERN.test(state)) {
|
|
176
|
+
throw new McpNativeOAuthError("invalid-configuration", "OAuth state must contain 32 to 512 URL-safe characters");
|
|
177
|
+
}
|
|
178
|
+
await this.#storage.saveOAuthState(state, this.#authorizationOwner);
|
|
179
|
+
return state;
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
try {
|
|
183
|
+
if (storageReserved) {
|
|
184
|
+
await this.#storage.clearOAuthState(this.#authorizationOwner);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch (cleanupError) {
|
|
188
|
+
throw new McpNativeOAuthError("invalid-storage", "OAuth state setup failed and its reservation could not be released", { cause: new AggregateError([error, cleanupError]) });
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
this.#authorizationAttemptReserved = false;
|
|
192
|
+
this.#authorizationCodeVerifierSaved = false;
|
|
193
|
+
}
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
this.#authorizationStateSetupRunning = false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async clientInformation(context) {
|
|
201
|
+
if (context === undefined) {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
const issuer = parseIssuer(context.issuer, "client-information issuer");
|
|
205
|
+
const stored = await this.#storage.loadClientInformation(issuer);
|
|
206
|
+
if (stored === undefined) {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
const parsed = parseStoredClientInformation(stored);
|
|
210
|
+
requireMatchingIssuer(parsed.issuer, issuer, "client information");
|
|
211
|
+
this.#activeIssuer = issuer;
|
|
212
|
+
return parsed;
|
|
213
|
+
}
|
|
214
|
+
async saveClientInformation(information, context) {
|
|
215
|
+
const parsed = parseStoredClientInformation(information);
|
|
216
|
+
const issuer = resolveStoredIssuer(parsed.issuer, context?.issuer, "client information");
|
|
217
|
+
this.#activeIssuer = issuer;
|
|
218
|
+
await this.#storage.saveClientInformation(issuer, { ...parsed, issuer });
|
|
219
|
+
}
|
|
220
|
+
async tokens(context) {
|
|
221
|
+
const requestedIssuer = context === undefined ? undefined : parseIssuer(context.issuer, "token issuer");
|
|
222
|
+
const stored = await this.#storage.loadTokens(requestedIssuer);
|
|
223
|
+
if (stored === undefined) {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
const parsed = parseStoredTokens(stored);
|
|
227
|
+
const issuer = parseIssuer(parsed.issuer, "stored token issuer");
|
|
228
|
+
if (requestedIssuer !== undefined) {
|
|
229
|
+
requireMatchingIssuer(issuer, requestedIssuer, "tokens");
|
|
230
|
+
}
|
|
231
|
+
this.#activeIssuer = issuer;
|
|
232
|
+
return parsed;
|
|
233
|
+
}
|
|
234
|
+
async saveTokens(tokens, context) {
|
|
235
|
+
const parsed = parseStoredTokens(tokens);
|
|
236
|
+
const issuer = resolveStoredIssuer(parsed.issuer, context?.issuer, "tokens");
|
|
237
|
+
this.#activeIssuer = issuer;
|
|
238
|
+
let effectiveScopes;
|
|
239
|
+
if (parsed.scope !== undefined) {
|
|
240
|
+
effectiveScopes = parseScope(parsed.scope);
|
|
241
|
+
}
|
|
242
|
+
else if (this.#authorizationCompletionRunning) {
|
|
243
|
+
const pending = await this.#storage.loadPendingAuthorization();
|
|
244
|
+
if (pending === undefined) {
|
|
245
|
+
throw new McpNativeOAuthError("invalid-storage", "Pending OAuth authorization context is missing");
|
|
246
|
+
}
|
|
247
|
+
effectiveScopes = parsePendingAuthorizationRecord(pending, this.#resourceUrl.href, issuer).scopes;
|
|
248
|
+
}
|
|
249
|
+
let previousTokens;
|
|
250
|
+
let storedScopeRecord;
|
|
251
|
+
if (parsed.scope === undefined && effectiveScopes === undefined) {
|
|
252
|
+
const stored = await this.#storage.loadTokens(issuer);
|
|
253
|
+
if (stored !== undefined) {
|
|
254
|
+
previousTokens = parseStoredTokens(stored);
|
|
255
|
+
requireMatchingIssuer(parseIssuer(previousTokens.issuer, "stored token issuer"), issuer, "tokens");
|
|
256
|
+
}
|
|
257
|
+
storedScopeRecord = await this.#loadScopeRecord();
|
|
258
|
+
effectiveScopes =
|
|
259
|
+
previousTokens?.scope === undefined
|
|
260
|
+
? storedScopeRecord?.scopes
|
|
261
|
+
: parseScope(previousTokens.scope);
|
|
262
|
+
}
|
|
263
|
+
const persistedTokens = parsed.scope === undefined && effectiveScopes !== undefined
|
|
264
|
+
? { ...parsed, issuer, scope: effectiveScopes.join(" ") }
|
|
265
|
+
: { ...parsed, issuer };
|
|
266
|
+
await this.#storage.saveTokens(issuer, persistedTokens);
|
|
267
|
+
if (this.#scopeStore !== undefined && effectiveScopes !== undefined) {
|
|
268
|
+
await this.#scopeStore.save(Object.freeze({
|
|
269
|
+
resource: this.#resourceUrl.href,
|
|
270
|
+
issuer,
|
|
271
|
+
scopes: Object.freeze([...effectiveScopes]),
|
|
272
|
+
}));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async redirectToAuthorization(authorizationUrl) {
|
|
276
|
+
const serializedAuthorizationUrl = authorizationUrl.href;
|
|
277
|
+
if (serializedAuthorizationUrl.length > MAX_AUTHORIZATION_URL_CODE_UNITS) {
|
|
278
|
+
throw new McpNativeOAuthError("invalid-configuration", "OAuth authorization URL exceeds the supported size");
|
|
279
|
+
}
|
|
280
|
+
const url = parseSecureEndpoint(serializedAuthorizationUrl, "authorization URL");
|
|
281
|
+
if (url.href.includes("#")) {
|
|
282
|
+
throw new McpNativeOAuthError("invalid-configuration", "OAuth authorization URL must not contain a fragment");
|
|
283
|
+
}
|
|
284
|
+
if (this.#authorizationStateSetupRunning ||
|
|
285
|
+
this.#authorizationCodeVerifierSetupRunning ||
|
|
286
|
+
this.#authorizationCleanupRunning ||
|
|
287
|
+
this.#authorizationCompletionRunning) {
|
|
288
|
+
throw new McpNativeOAuthError("invalid-configuration", "OAuth authorization setup, callback completion, or cleanup is already running");
|
|
289
|
+
}
|
|
290
|
+
if (!this.#authorizationAttemptReserved || !this.#authorizationCodeVerifierSaved) {
|
|
291
|
+
throw new McpNativeOAuthError("invalid-configuration", "OAuth authorization handoff requires reserved state and one saved PKCE verifier");
|
|
292
|
+
}
|
|
293
|
+
if (this.#pendingAuthorizationUrl === url.href) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (this.#pendingAuthorizationUrl !== undefined) {
|
|
297
|
+
throw new McpNativeOAuthError("invalid-configuration", "Another OAuth authorization URL is already pending");
|
|
298
|
+
}
|
|
299
|
+
if (this.#authorizationHandoffRunning) {
|
|
300
|
+
throw new McpNativeOAuthError("invalid-configuration", "Another OAuth authorization handoff is already running");
|
|
301
|
+
}
|
|
302
|
+
this.#authorizationHandoffRunning = true;
|
|
303
|
+
try {
|
|
304
|
+
const requestedScopes = parseScope(url.searchParams.get("scope"));
|
|
305
|
+
const storedTokens = await this.#storage.loadTokens(this.#activeIssuer);
|
|
306
|
+
let parsedStoredTokens;
|
|
307
|
+
if (storedTokens !== undefined) {
|
|
308
|
+
parsedStoredTokens = parseStoredTokens(storedTokens);
|
|
309
|
+
const storedIssuer = parseIssuer(parsedStoredTokens.issuer, "stored token issuer");
|
|
310
|
+
if (this.#activeIssuer === undefined)
|
|
311
|
+
this.#activeIssuer = storedIssuer;
|
|
312
|
+
else
|
|
313
|
+
requireMatchingIssuer(storedIssuer, this.#activeIssuer, "tokens");
|
|
314
|
+
}
|
|
315
|
+
const storedScopeRecord = await this.#loadScopeRecord();
|
|
316
|
+
if (parsedStoredTokens !== undefined || storedScopeRecord !== undefined) {
|
|
317
|
+
const currentScopes = parsedStoredTokens?.scope === undefined
|
|
318
|
+
? (storedScopeRecord?.scopes ?? [])
|
|
319
|
+
: parseScope(parsedStoredTokens.scope);
|
|
320
|
+
const currentScopeSet = new Set(currentScopes);
|
|
321
|
+
const addedScopes = requestedScopes.filter((scope) => !currentScopeSet.has(scope));
|
|
322
|
+
const approved = await this.#approveReauthorization?.(Object.freeze({
|
|
323
|
+
resource: this.#resourceUrl.href,
|
|
324
|
+
issuer: this.#activeIssuer,
|
|
325
|
+
currentScopes: Object.freeze([...currentScopes]),
|
|
326
|
+
requestedScopes: Object.freeze([...requestedScopes]),
|
|
327
|
+
addedScopes: Object.freeze([...addedScopes]),
|
|
328
|
+
}));
|
|
329
|
+
if (approved !== true) {
|
|
330
|
+
throw new McpNativeOAuthError("reauthorization-denied", "OAuth reauthorization requires explicit host approval");
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
await this.#storage.savePendingAuthorization(Object.freeze({
|
|
334
|
+
resource: this.#resourceUrl.href,
|
|
335
|
+
...(this.#activeIssuer === undefined ? {} : { issuer: this.#activeIssuer }),
|
|
336
|
+
scopes: Object.freeze([...requestedScopes]),
|
|
337
|
+
}));
|
|
338
|
+
await this.#openAuthorization(new URL(url.href));
|
|
339
|
+
this.#pendingAuthorizationUrl = url.href;
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
await this.#discardPendingAuthorization();
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
finally {
|
|
346
|
+
this.#authorizationHandoffRunning = false;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
async saveCodeVerifier(verifier) {
|
|
350
|
+
assertCodeVerifier(verifier);
|
|
351
|
+
if (!this.#authorizationAttemptReserved ||
|
|
352
|
+
this.#authorizationCodeVerifierSaved ||
|
|
353
|
+
this.#authorizationStateSetupRunning ||
|
|
354
|
+
this.#authorizationCodeVerifierSetupRunning ||
|
|
355
|
+
this.#authorizationCleanupRunning ||
|
|
356
|
+
this.#authorizationCompletionRunning ||
|
|
357
|
+
this.#authorizationHandoffRunning ||
|
|
358
|
+
this.#pendingAuthorizationUrl !== undefined) {
|
|
359
|
+
throw new McpNativeOAuthError("invalid-configuration", "OAuth PKCE verifier must belong to one reserved authorization attempt");
|
|
360
|
+
}
|
|
361
|
+
this.#authorizationCodeVerifierSetupRunning = true;
|
|
362
|
+
try {
|
|
363
|
+
await this.#storage.saveCodeVerifier(verifier);
|
|
364
|
+
this.#authorizationCodeVerifierSaved = true;
|
|
365
|
+
}
|
|
366
|
+
finally {
|
|
367
|
+
this.#authorizationCodeVerifierSetupRunning = false;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
async codeVerifier() {
|
|
371
|
+
const verifier = await this.#storage.loadCodeVerifier();
|
|
372
|
+
if (verifier === undefined) {
|
|
373
|
+
throw new McpNativeOAuthError("invalid-storage", "OAuth PKCE verifier is missing");
|
|
374
|
+
}
|
|
375
|
+
assertCodeVerifier(verifier);
|
|
376
|
+
return verifier;
|
|
377
|
+
}
|
|
378
|
+
async validateResourceURL(serverUrl, resource) {
|
|
379
|
+
const requestedServer = resourceUrlFromServerUrl(parseProtectedServerUrl(serverUrl));
|
|
380
|
+
if (requestedServer.href !== this.#resourceUrl.href) {
|
|
381
|
+
throw new McpNativeOAuthError("resource-mismatch", "OAuth provider cannot be reused for a different MCP server");
|
|
382
|
+
}
|
|
383
|
+
if (resource !== undefined) {
|
|
384
|
+
if (typeof resource !== "string" || resource.length > MAX_RESOURCE_IDENTIFIER_CODE_UNITS) {
|
|
385
|
+
throw new McpNativeOAuthError("invalid-storage", "Protected resource identifier is invalid or exceeds the supported size");
|
|
386
|
+
}
|
|
387
|
+
let allowed;
|
|
388
|
+
try {
|
|
389
|
+
allowed = checkResourceAllowed({
|
|
390
|
+
requestedResource: this.#resourceUrl,
|
|
391
|
+
configuredResource: resource,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
throw new McpNativeOAuthError("invalid-storage", "Protected resource metadata contains an invalid resource identifier", { cause: error });
|
|
396
|
+
}
|
|
397
|
+
if (!allowed) {
|
|
398
|
+
throw new McpNativeOAuthError("resource-mismatch", "Protected resource metadata does not identify the configured MCP server");
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return new URL(this.#resourceUrl.href);
|
|
402
|
+
}
|
|
403
|
+
async saveDiscoveryState(state) {
|
|
404
|
+
const parsed = parseDiscoveryState(state, this.#resourceUrl);
|
|
405
|
+
this.#activeIssuer = parsed.authorizationServerUrl;
|
|
406
|
+
await this.#storage.saveDiscoveryState(parsed);
|
|
407
|
+
}
|
|
408
|
+
async discoveryState() {
|
|
409
|
+
const state = await this.#storage.loadDiscoveryState();
|
|
410
|
+
if (state === undefined) {
|
|
411
|
+
return undefined;
|
|
412
|
+
}
|
|
413
|
+
const parsed = parseDiscoveryState(state, this.#resourceUrl);
|
|
414
|
+
const verifier = await this.#storage.loadCodeVerifier();
|
|
415
|
+
if (verifier === undefined) {
|
|
416
|
+
return undefined;
|
|
417
|
+
}
|
|
418
|
+
assertCodeVerifier(verifier);
|
|
419
|
+
this.#activeIssuer = parsed.authorizationServerUrl;
|
|
420
|
+
return parsed;
|
|
421
|
+
}
|
|
422
|
+
async invalidateCredentials(scope) {
|
|
423
|
+
const clearsAuthorizationAttempt = scope === "all" || scope === "verifier";
|
|
424
|
+
if (clearsAuthorizationAttempt) {
|
|
425
|
+
if (this.#authorizationStateSetupRunning ||
|
|
426
|
+
this.#authorizationCodeVerifierSetupRunning ||
|
|
427
|
+
this.#authorizationCleanupRunning ||
|
|
428
|
+
this.#authorizationCompletionRunning ||
|
|
429
|
+
this.#authorizationHandoffRunning) {
|
|
430
|
+
throw new McpNativeOAuthError("invalid-configuration", "OAuth authorization state/verifier setup, handoff, callback completion, or cleanup is already running");
|
|
431
|
+
}
|
|
432
|
+
this.#authorizationCleanupRunning = true;
|
|
433
|
+
}
|
|
434
|
+
try {
|
|
435
|
+
if (clearsAuthorizationAttempt) {
|
|
436
|
+
await this.#storage.claimOAuthStateForCleanup(this.#authorizationOwner);
|
|
437
|
+
}
|
|
438
|
+
await this.#storage.invalidate(scope, this.#activeIssuer);
|
|
439
|
+
if (scope === "verifier") {
|
|
440
|
+
await this.#storage.clearOAuthState(this.#authorizationOwner);
|
|
441
|
+
this.#pendingAuthorizationUrl = undefined;
|
|
442
|
+
this.#authorizationAttemptReserved = false;
|
|
443
|
+
this.#authorizationCodeVerifierSaved = false;
|
|
444
|
+
}
|
|
445
|
+
if (scope === "all" || scope === "discovery") {
|
|
446
|
+
this.#activeIssuer = undefined;
|
|
447
|
+
}
|
|
448
|
+
if (scope === "all") {
|
|
449
|
+
await this.#scopeStore?.remove(this.#resourceUrl.href);
|
|
450
|
+
this.#pendingAuthorizationUrl = undefined;
|
|
451
|
+
this.#authorizationAttemptReserved = false;
|
|
452
|
+
this.#authorizationCodeVerifierSaved = false;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
finally {
|
|
456
|
+
if (clearsAuthorizationAttempt) {
|
|
457
|
+
this.#authorizationCleanupRunning = false;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Clears an abandoned interactive attempt after the platform handoff has settled, including a
|
|
463
|
+
* reservation persisted by an earlier process. Active state setup, handoff, and callback
|
|
464
|
+
* completion cannot be cancelled through this method.
|
|
465
|
+
*/
|
|
466
|
+
async cancelAuthorization() {
|
|
467
|
+
if (this.#authorizationStateSetupRunning ||
|
|
468
|
+
this.#authorizationCodeVerifierSetupRunning ||
|
|
469
|
+
this.#authorizationCleanupRunning ||
|
|
470
|
+
this.#authorizationCompletionRunning ||
|
|
471
|
+
this.#authorizationHandoffRunning) {
|
|
472
|
+
throw new McpNativeOAuthError("invalid-configuration", "OAuth authorization state/verifier setup, handoff, callback completion, or cleanup is already running");
|
|
473
|
+
}
|
|
474
|
+
await this.#discardPendingAuthorization();
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Validates an app/loopback callback before asking the SDK to redeem its code.
|
|
478
|
+
* OAuth state is consumed before token exchange, so the callback cannot be replayed.
|
|
479
|
+
*/
|
|
480
|
+
async finishAuthorization(finisher, callbackUrl) {
|
|
481
|
+
if (this.#authorizationStateSetupRunning ||
|
|
482
|
+
this.#authorizationCodeVerifierSetupRunning ||
|
|
483
|
+
this.#authorizationHandoffRunning ||
|
|
484
|
+
this.#authorizationCompletionRunning ||
|
|
485
|
+
this.#authorizationCleanupRunning) {
|
|
486
|
+
throw new McpNativeOAuthError("invalid-callback", "OAuth authorization setup, handoff, callback completion, or cleanup is already running");
|
|
487
|
+
}
|
|
488
|
+
this.#authorizationCompletionRunning = true;
|
|
489
|
+
const reservedForRecovery = !this.#authorizationAttemptReserved;
|
|
490
|
+
this.#authorizationAttemptReserved = true;
|
|
491
|
+
let stateClaimed = false;
|
|
492
|
+
try {
|
|
493
|
+
const callback = parseCallbackUrl(callbackUrl, this.redirectUrl);
|
|
494
|
+
const parameters = callback.searchParams;
|
|
495
|
+
requireSingleParameter(parameters, "state");
|
|
496
|
+
if (!(await this.#storage.consumeOAuthState(parameters.get("state"), this.#authorizationOwner))) {
|
|
497
|
+
throw new McpNativeOAuthError("state-mismatch", "OAuth callback state did not match");
|
|
498
|
+
}
|
|
499
|
+
stateClaimed = true;
|
|
500
|
+
try {
|
|
501
|
+
if (parameters.has("error")) {
|
|
502
|
+
throw new McpNativeOAuthError("authorization-denied", "The authorization server did not grant access");
|
|
503
|
+
}
|
|
504
|
+
requireSingleParameter(parameters, "code");
|
|
505
|
+
if (parameters.has("iss")) {
|
|
506
|
+
requireSingleParameter(parameters, "iss");
|
|
507
|
+
}
|
|
508
|
+
await finisher.finishAuth(parameters);
|
|
509
|
+
}
|
|
510
|
+
finally {
|
|
511
|
+
this.#pendingAuthorizationUrl = undefined;
|
|
512
|
+
try {
|
|
513
|
+
await this.#storage.invalidate("verifier", this.#activeIssuer);
|
|
514
|
+
await this.#storage.clearOAuthState(this.#authorizationOwner);
|
|
515
|
+
}
|
|
516
|
+
finally {
|
|
517
|
+
this.#authorizationAttemptReserved = false;
|
|
518
|
+
this.#authorizationCodeVerifierSaved = false;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
finally {
|
|
523
|
+
this.#authorizationCompletionRunning = false;
|
|
524
|
+
if (!stateClaimed && reservedForRecovery) {
|
|
525
|
+
this.#authorizationAttemptReserved = false;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
assertServerUrl(serverUrl) {
|
|
530
|
+
const parsed = parseProtectedServerUrl(serverUrl);
|
|
531
|
+
if (resourceUrlFromServerUrl(parsed).href !== this.#resourceUrl.href) {
|
|
532
|
+
throw new McpNativeOAuthError("resource-mismatch", "OAuth transport URL does not match the provider's MCP server");
|
|
533
|
+
}
|
|
534
|
+
return parsed;
|
|
535
|
+
}
|
|
536
|
+
hasReauthorizationApproval() {
|
|
537
|
+
return this.#approveReauthorization !== undefined;
|
|
538
|
+
}
|
|
539
|
+
async #loadScopeRecord() {
|
|
540
|
+
if (this.#scopeStore === undefined)
|
|
541
|
+
return undefined;
|
|
542
|
+
const value = await this.#scopeStore.load(this.#resourceUrl.href);
|
|
543
|
+
if (value === undefined)
|
|
544
|
+
return undefined;
|
|
545
|
+
const record = parseScopeRecord(value, this.#resourceUrl.href, this.#activeIssuer);
|
|
546
|
+
this.#activeIssuer ??= record.issuer;
|
|
547
|
+
return record;
|
|
548
|
+
}
|
|
549
|
+
async #discardPendingAuthorization() {
|
|
550
|
+
if (this.#authorizationCleanupRunning) {
|
|
551
|
+
throw new McpNativeOAuthError("invalid-configuration", "OAuth authorization cleanup is already running");
|
|
552
|
+
}
|
|
553
|
+
this.#authorizationCleanupRunning = true;
|
|
554
|
+
this.#pendingAuthorizationUrl = undefined;
|
|
555
|
+
try {
|
|
556
|
+
await this.#storage.claimOAuthStateForCleanup(this.#authorizationOwner);
|
|
557
|
+
await this.#storage.invalidate("verifier", this.#activeIssuer);
|
|
558
|
+
await this.#storage.clearOAuthState(this.#authorizationOwner);
|
|
559
|
+
}
|
|
560
|
+
finally {
|
|
561
|
+
this.#authorizationAttemptReserved = false;
|
|
562
|
+
this.#authorizationCodeVerifierSaved = false;
|
|
563
|
+
this.#authorizationCleanupRunning = false;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
export function createMcpNativeOAuthProvider(options) {
|
|
568
|
+
return new McpNativeOAuthClientProvider(options);
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Creates the supported protected Streamable HTTP transport profile.
|
|
572
|
+
* Scope step-up is surfaced to the host so consent can be obtained before reauthorization.
|
|
573
|
+
*/
|
|
574
|
+
export function createMcpNativeOAuthTransport(serverUrl, provider, options = {}) {
|
|
575
|
+
const url = provider.assertServerUrl(serverUrl);
|
|
576
|
+
const headers = parseNonCredentialHeaders(options.headers);
|
|
577
|
+
const scopeEscalation = options.scopeEscalation ?? "throw";
|
|
578
|
+
if (scopeEscalation === "host-approved" && !provider.hasReauthorizationApproval()) {
|
|
579
|
+
throw new McpNativeOAuthError("invalid-configuration", "Host-approved OAuth reauthorization requires an approval callback");
|
|
580
|
+
}
|
|
581
|
+
return new StreamableHTTPClientTransport(url, {
|
|
582
|
+
authProvider: provider,
|
|
583
|
+
onInsufficientScope: scopeEscalation === "host-approved" ? "reauthorize" : "throw",
|
|
584
|
+
maxStepUpRetries: 1,
|
|
585
|
+
...(headers === undefined ? {} : { requestInit: { headers } }),
|
|
586
|
+
...(options.fetch === undefined ? {} : { fetch: options.fetch }),
|
|
587
|
+
...(options.reconnectionOptions === undefined
|
|
588
|
+
? {}
|
|
589
|
+
: { reconnectionOptions: options.reconnectionOptions }),
|
|
590
|
+
...(options.reconnectionScheduler === undefined
|
|
591
|
+
? {}
|
|
592
|
+
: { reconnectionScheduler: options.reconnectionScheduler }),
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
function parseScope(value) {
|
|
596
|
+
if (value === null || value === "") {
|
|
597
|
+
return [];
|
|
598
|
+
}
|
|
599
|
+
if (value.length > MAX_SCOPE_CODE_UNITS) {
|
|
600
|
+
throw new McpNativeOAuthError("invalid-configuration", "OAuth authorization scope exceeds the supported size");
|
|
601
|
+
}
|
|
602
|
+
const scopes = value.split(" ");
|
|
603
|
+
if (scopes.length > MAX_SCOPE_TOKENS ||
|
|
604
|
+
scopes.some((scope) => !SCOPE_TOKEN_PATTERN.test(scope)) ||
|
|
605
|
+
new Set(scopes).size !== scopes.length) {
|
|
606
|
+
throw new McpNativeOAuthError("invalid-configuration", "OAuth authorization scope is invalid or exceeds the supported limits");
|
|
607
|
+
}
|
|
608
|
+
return scopes;
|
|
609
|
+
}
|
|
610
|
+
function parseScopeRecord(value, expectedResource, expectedIssuer) {
|
|
611
|
+
if (value === null ||
|
|
612
|
+
typeof value !== "object" ||
|
|
613
|
+
Array.isArray(value) ||
|
|
614
|
+
(Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)) {
|
|
615
|
+
throw new McpNativeOAuthError("invalid-storage", "Stored OAuth scope record is invalid");
|
|
616
|
+
}
|
|
617
|
+
const record = value;
|
|
618
|
+
if (Object.keys(record).length !== 3 ||
|
|
619
|
+
!Object.hasOwn(record, "resource") ||
|
|
620
|
+
!Object.hasOwn(record, "issuer") ||
|
|
621
|
+
!Object.hasOwn(record, "scopes") ||
|
|
622
|
+
typeof record.resource !== "string" ||
|
|
623
|
+
record.resource !== expectedResource ||
|
|
624
|
+
typeof record.issuer !== "string") {
|
|
625
|
+
throw new McpNativeOAuthError("invalid-storage", "Stored OAuth scope record is invalid");
|
|
626
|
+
}
|
|
627
|
+
const storedScopes = parseStoredScopeArray(record.scopes, "Stored OAuth scopes");
|
|
628
|
+
let issuer;
|
|
629
|
+
try {
|
|
630
|
+
issuer = parseIssuer(record.issuer, "stored scope issuer");
|
|
631
|
+
}
|
|
632
|
+
catch (error) {
|
|
633
|
+
throw new McpNativeOAuthError("invalid-storage", "Stored OAuth scope issuer is invalid", {
|
|
634
|
+
cause: error,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
if (expectedIssuer !== undefined) {
|
|
638
|
+
requireMatchingIssuer(issuer, expectedIssuer, "scope history");
|
|
639
|
+
}
|
|
640
|
+
let scopes;
|
|
641
|
+
try {
|
|
642
|
+
scopes = parseScope(storedScopes.join(" "));
|
|
643
|
+
}
|
|
644
|
+
catch (error) {
|
|
645
|
+
throw new McpNativeOAuthError("invalid-storage", "Stored OAuth scopes are invalid", {
|
|
646
|
+
cause: error,
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
if (scopes.length !== storedScopes.length ||
|
|
650
|
+
scopes.some((scope, index) => scope !== storedScopes[index])) {
|
|
651
|
+
throw new McpNativeOAuthError("invalid-storage", "Stored OAuth scopes are invalid");
|
|
652
|
+
}
|
|
653
|
+
return Object.freeze({
|
|
654
|
+
resource: expectedResource,
|
|
655
|
+
issuer,
|
|
656
|
+
scopes: Object.freeze([...scopes]),
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
function parsePendingAuthorizationRecord(value, expectedResource, expectedIssuer) {
|
|
660
|
+
if (value === null ||
|
|
661
|
+
typeof value !== "object" ||
|
|
662
|
+
Array.isArray(value) ||
|
|
663
|
+
(Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)) {
|
|
664
|
+
throw new McpNativeOAuthError("invalid-storage", "Stored pending OAuth authorization record is invalid");
|
|
665
|
+
}
|
|
666
|
+
const record = value;
|
|
667
|
+
const keys = Object.keys(record);
|
|
668
|
+
if ((keys.length !== 2 && keys.length !== 3) ||
|
|
669
|
+
!Object.hasOwn(record, "resource") ||
|
|
670
|
+
!Object.hasOwn(record, "scopes") ||
|
|
671
|
+
(keys.length === 3 && !Object.hasOwn(record, "issuer")) ||
|
|
672
|
+
typeof record.resource !== "string" ||
|
|
673
|
+
record.resource !== expectedResource) {
|
|
674
|
+
throw new McpNativeOAuthError("invalid-storage", "Stored pending OAuth authorization record is invalid");
|
|
675
|
+
}
|
|
676
|
+
const storedScopes = parseStoredScopeArray(record.scopes, "Stored pending OAuth authorization scopes");
|
|
677
|
+
let issuer;
|
|
678
|
+
if (Object.hasOwn(record, "issuer")) {
|
|
679
|
+
try {
|
|
680
|
+
issuer = parseIssuer(record.issuer, "pending authorization issuer");
|
|
681
|
+
requireMatchingIssuer(issuer, expectedIssuer, "pending authorization");
|
|
682
|
+
}
|
|
683
|
+
catch (error) {
|
|
684
|
+
throw new McpNativeOAuthError("invalid-storage", "Stored pending OAuth authorization issuer is invalid", { cause: error });
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
let scopes;
|
|
688
|
+
try {
|
|
689
|
+
scopes = parseScope(storedScopes.join(" "));
|
|
690
|
+
}
|
|
691
|
+
catch (error) {
|
|
692
|
+
throw new McpNativeOAuthError("invalid-storage", "Stored pending OAuth authorization scopes are invalid", { cause: error });
|
|
693
|
+
}
|
|
694
|
+
if (scopes.length !== storedScopes.length ||
|
|
695
|
+
scopes.some((scope, index) => scope !== storedScopes[index])) {
|
|
696
|
+
throw new McpNativeOAuthError("invalid-storage", "Stored pending OAuth authorization scopes are invalid");
|
|
697
|
+
}
|
|
698
|
+
return Object.freeze({
|
|
699
|
+
resource: expectedResource,
|
|
700
|
+
...(issuer === undefined ? {} : { issuer }),
|
|
701
|
+
scopes: Object.freeze([...scopes]),
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
function parseStoredScopeArray(value, label) {
|
|
705
|
+
if (!Array.isArray(value) || value.length > MAX_SCOPE_TOKENS) {
|
|
706
|
+
throw new McpNativeOAuthError("invalid-storage", `${label} are invalid or exceed the supported limits`);
|
|
707
|
+
}
|
|
708
|
+
const scopes = [];
|
|
709
|
+
let totalCodeUnits = Math.max(0, value.length - 1);
|
|
710
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
711
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, index);
|
|
712
|
+
if (descriptor === undefined ||
|
|
713
|
+
!("value" in descriptor) ||
|
|
714
|
+
typeof descriptor.value !== "string" ||
|
|
715
|
+
descriptor.value.length > 256) {
|
|
716
|
+
throw new McpNativeOAuthError("invalid-storage", `${label} are invalid or exceed the supported limits`);
|
|
717
|
+
}
|
|
718
|
+
totalCodeUnits += descriptor.value.length;
|
|
719
|
+
if (totalCodeUnits > MAX_SCOPE_CODE_UNITS) {
|
|
720
|
+
throw new McpNativeOAuthError("invalid-storage", `${label} are invalid or exceed the supported limits`);
|
|
721
|
+
}
|
|
722
|
+
scopes.push(descriptor.value);
|
|
723
|
+
}
|
|
724
|
+
let enumerableProperties = 0;
|
|
725
|
+
for (const key in value) {
|
|
726
|
+
if (!Object.hasOwn(value, key))
|
|
727
|
+
continue;
|
|
728
|
+
enumerableProperties += 1;
|
|
729
|
+
if (enumerableProperties > value.length) {
|
|
730
|
+
throw new McpNativeOAuthError("invalid-storage", `${label} are invalid or exceed the supported limits`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
if (enumerableProperties !== value.length) {
|
|
734
|
+
throw new McpNativeOAuthError("invalid-storage", `${label} are invalid or exceed the supported limits`);
|
|
735
|
+
}
|
|
736
|
+
return scopes;
|
|
737
|
+
}
|
|
738
|
+
function parseProtectedServerUrl(value) {
|
|
739
|
+
const url = parseSecureEndpoint(value, "MCP server URL");
|
|
740
|
+
if (url.href.includes("#")) {
|
|
741
|
+
throw new McpNativeOAuthError("invalid-configuration", "MCP server URL must not contain a fragment");
|
|
742
|
+
}
|
|
743
|
+
return url;
|
|
744
|
+
}
|
|
745
|
+
function parseRedirectUrl(value) {
|
|
746
|
+
const url = parseUrl(value, "OAuth redirect URL");
|
|
747
|
+
if (url.username !== "" || url.password !== "" || url.href.includes("#")) {
|
|
748
|
+
throw new McpNativeOAuthError("invalid-configuration", "OAuth redirect URL must not contain credentials or a fragment");
|
|
749
|
+
}
|
|
750
|
+
if (!isSupportedRedirectLocation(url)) {
|
|
751
|
+
throw new McpNativeOAuthError("invalid-configuration", "OAuth redirect URL must use HTTPS, HTTP loopback, or a safe private-use app scheme");
|
|
752
|
+
}
|
|
753
|
+
assertUniqueRedirectParameters(url);
|
|
754
|
+
for (const name of CALLBACK_PARAMETER_NAMES) {
|
|
755
|
+
if (url.searchParams.has(name)) {
|
|
756
|
+
throw new McpNativeOAuthError("invalid-configuration", `OAuth redirect URL must not predefine callback parameter ${name}`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
assertOAuthRedirectParameterBudget(url);
|
|
760
|
+
return url;
|
|
761
|
+
}
|
|
762
|
+
function assertUniqueRedirectParameters(url) {
|
|
763
|
+
const names = new Set();
|
|
764
|
+
for (const name of url.searchParams.keys()) {
|
|
765
|
+
if (names.has(name)) {
|
|
766
|
+
throw new McpNativeOAuthError("invalid-configuration", "OAuth redirect URL must not contain duplicate query parameter names");
|
|
767
|
+
}
|
|
768
|
+
names.add(name);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
function parseSecureEndpoint(value, label) {
|
|
772
|
+
const url = parseUrl(value, label);
|
|
773
|
+
if (url.username !== "" || url.password !== "") {
|
|
774
|
+
throw new McpNativeOAuthError("invalid-configuration", `${label} must not contain credentials`);
|
|
775
|
+
}
|
|
776
|
+
if (url.protocol !== "https:" &&
|
|
777
|
+
!(url.protocol === "http:" && isLoopbackHostname(url.hostname))) {
|
|
778
|
+
throw new McpNativeOAuthError("invalid-configuration", `${label} must use HTTPS or an HTTP loopback address`);
|
|
779
|
+
}
|
|
780
|
+
return url;
|
|
781
|
+
}
|
|
782
|
+
function parseUrl(value, label) {
|
|
783
|
+
try {
|
|
784
|
+
return new URL(value instanceof URL ? value.href : value);
|
|
785
|
+
}
|
|
786
|
+
catch (error) {
|
|
787
|
+
throw new McpNativeOAuthError("invalid-configuration", `${label} must be an absolute URL`, {
|
|
788
|
+
cause: error,
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
function isSupportedRedirectLocation(url) {
|
|
793
|
+
if (url.protocol === "https:")
|
|
794
|
+
return true;
|
|
795
|
+
if (url.protocol === "http:")
|
|
796
|
+
return isLoopbackHostname(url.hostname);
|
|
797
|
+
return (!RESERVED_REDIRECT_SCHEMES.has(url.protocol) &&
|
|
798
|
+
PRIVATE_USE_REDIRECT_SCHEME_PATTERN.test(url.protocol) &&
|
|
799
|
+
url.hostname !== "" &&
|
|
800
|
+
url.port === "");
|
|
801
|
+
}
|
|
802
|
+
function parseIssuer(value, label) {
|
|
803
|
+
if (typeof value !== "string" ||
|
|
804
|
+
value.length === 0 ||
|
|
805
|
+
value.length > MAX_OAUTH_ISSUER_CODE_UNITS) {
|
|
806
|
+
throw new McpNativeOAuthError("invalid-storage", `${label} is missing, invalid, or too large`);
|
|
807
|
+
}
|
|
808
|
+
const url = parseStoredSecureEndpoint(value, label);
|
|
809
|
+
if (url.href.includes("?") || url.href.includes("#")) {
|
|
810
|
+
throw new McpNativeOAuthError("invalid-storage", `${label} must not contain a query or fragment`);
|
|
811
|
+
}
|
|
812
|
+
return value;
|
|
813
|
+
}
|
|
814
|
+
function resolveStoredIssuer(storedIssuer, contextIssuer, label) {
|
|
815
|
+
const issuer = parseIssuer(contextIssuer ?? storedIssuer, `${label} issuer`);
|
|
816
|
+
if (storedIssuer !== undefined) {
|
|
817
|
+
requireMatchingIssuer(parseIssuer(storedIssuer, `${label} stored issuer`), issuer, label);
|
|
818
|
+
}
|
|
819
|
+
if (contextIssuer !== undefined) {
|
|
820
|
+
requireMatchingIssuer(parseIssuer(contextIssuer, `${label} context issuer`), issuer, label);
|
|
821
|
+
}
|
|
822
|
+
return issuer;
|
|
823
|
+
}
|
|
824
|
+
function requireMatchingIssuer(actual, expected, label) {
|
|
825
|
+
if (actual !== expected) {
|
|
826
|
+
throw new McpNativeOAuthError("invalid-storage", `Stored ${label} belongs to a different authorization server`);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
function parseStoredTokens(value) {
|
|
830
|
+
assertBoundedOAuthJson(value, "Stored OAuth token response", TOKEN_INFORMATION_BUDGET);
|
|
831
|
+
assertOAuthRecord(value, "Stored OAuth token response");
|
|
832
|
+
assertBoundedTokenValues(value);
|
|
833
|
+
const result = OAuthTokensSchema.safeParse(value);
|
|
834
|
+
if (!result.success) {
|
|
835
|
+
throw new McpNativeOAuthError("invalid-storage", "Stored OAuth tokens are invalid", {
|
|
836
|
+
cause: result.error,
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
const issuer = Object.hasOwn(value, "issuer") ? value.issuer : undefined;
|
|
840
|
+
parseScope(result.data.scope ?? null);
|
|
841
|
+
return { ...result.data, ...(issuer === undefined ? {} : { issuer }) };
|
|
842
|
+
}
|
|
843
|
+
function assertBoundedTokenValues(value) {
|
|
844
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
845
|
+
return;
|
|
846
|
+
const record = value;
|
|
847
|
+
let total = 0;
|
|
848
|
+
for (const [field, limit] of Object.entries(TOKEN_STRING_LIMITS)) {
|
|
849
|
+
const fieldValue = record[field];
|
|
850
|
+
if (typeof fieldValue !== "string")
|
|
851
|
+
continue;
|
|
852
|
+
if (fieldValue.length > limit) {
|
|
853
|
+
throw new McpNativeOAuthError("invalid-storage", `Stored OAuth ${field} exceeds the supported size`);
|
|
854
|
+
}
|
|
855
|
+
total += fieldValue.length;
|
|
856
|
+
if (total > MAX_TOKEN_CUMULATIVE_CODE_UNITS) {
|
|
857
|
+
throw new McpNativeOAuthError("invalid-storage", "Stored OAuth token values exceed the cumulative supported size");
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
function parseStoredClientInformation(value) {
|
|
862
|
+
assertBoundedOAuthJson(value, "Stored OAuth client information", CLIENT_INFORMATION_BUDGET);
|
|
863
|
+
assertOAuthRecord(value, "Stored OAuth client information");
|
|
864
|
+
if (typeof value.client_id === "string" &&
|
|
865
|
+
value.client_id.length > MAX_CLIENT_IDENTIFIER_CODE_UNITS) {
|
|
866
|
+
throw new McpNativeOAuthError("invalid-storage", "Stored OAuth client identifier exceeds the supported size");
|
|
867
|
+
}
|
|
868
|
+
if (typeof value.client_secret === "string" &&
|
|
869
|
+
value.client_secret.length > MAX_CLIENT_SECRET_CODE_UNITS) {
|
|
870
|
+
throw new McpNativeOAuthError("invalid-storage", "Stored OAuth client secret exceeds the supported size");
|
|
871
|
+
}
|
|
872
|
+
const full = OAuthClientInformationFullSchema.safeParse(value);
|
|
873
|
+
const issuer = Object.hasOwn(value, "issuer") ? value.issuer : undefined;
|
|
874
|
+
if (full.success) {
|
|
875
|
+
return { ...full.data, ...(issuer === undefined ? {} : { issuer }) };
|
|
876
|
+
}
|
|
877
|
+
const basic = OAuthClientInformationSchema.safeParse(value);
|
|
878
|
+
if (!basic.success) {
|
|
879
|
+
throw new McpNativeOAuthError("invalid-storage", "Stored OAuth client information is invalid", {
|
|
880
|
+
cause: basic.error,
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
return { ...basic.data, ...(issuer === undefined ? {} : { issuer }) };
|
|
884
|
+
}
|
|
885
|
+
function parseDiscoveryState(state, resourceUrl) {
|
|
886
|
+
assertBoundedOAuthJson(state, "Stored OAuth discovery state", DISCOVERY_STATE_BUDGET);
|
|
887
|
+
assertOAuthRecord(state, "Stored OAuth discovery state");
|
|
888
|
+
const authorizationServerUrl = parseIssuer(state.authorizationServerUrl, "discovery authorization-server issuer");
|
|
889
|
+
const authorizationServerMetadata = parseAuthorizationServerMetadata(state.authorizationServerMetadata);
|
|
890
|
+
if (authorizationServerMetadata?.issuer !== undefined &&
|
|
891
|
+
authorizationServerMetadata.issuer !== authorizationServerUrl) {
|
|
892
|
+
throw new McpNativeOAuthError("invalid-storage", "Stored authorization-server metadata has a mismatched issuer");
|
|
893
|
+
}
|
|
894
|
+
const resourceMetadataResult = state.resourceMetadata === undefined
|
|
895
|
+
? undefined
|
|
896
|
+
: OAuthProtectedResourceMetadataSchema.safeParse(state.resourceMetadata);
|
|
897
|
+
if (resourceMetadataResult !== undefined && !resourceMetadataResult.success) {
|
|
898
|
+
throw new McpNativeOAuthError("invalid-storage", "Stored protected-resource metadata is invalid", { cause: resourceMetadataResult.error });
|
|
899
|
+
}
|
|
900
|
+
if (resourceMetadataResult?.success) {
|
|
901
|
+
let allowed;
|
|
902
|
+
try {
|
|
903
|
+
allowed = checkResourceAllowed({
|
|
904
|
+
requestedResource: resourceUrl,
|
|
905
|
+
configuredResource: resourceMetadataResult.data.resource,
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
catch (error) {
|
|
909
|
+
throw new McpNativeOAuthError("invalid-storage", "Stored protected-resource metadata contains an invalid resource identifier", { cause: error });
|
|
910
|
+
}
|
|
911
|
+
if (!allowed) {
|
|
912
|
+
throw new McpNativeOAuthError("resource-mismatch", "Stored protected-resource metadata identifies a different MCP server");
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
if (resourceMetadataResult?.success &&
|
|
916
|
+
resourceMetadataResult.data.authorization_servers !== undefined &&
|
|
917
|
+
!resourceMetadataResult.data.authorization_servers.includes(authorizationServerUrl)) {
|
|
918
|
+
throw new McpNativeOAuthError("invalid-storage", "Stored protected-resource metadata does not advertise the selected authorization server");
|
|
919
|
+
}
|
|
920
|
+
let resourceMetadataUrl;
|
|
921
|
+
if (state.resourceMetadataUrl !== undefined) {
|
|
922
|
+
const parsedResourceMetadataUrl = parseStoredSecureEndpoint(state.resourceMetadataUrl, "protected-resource metadata URL");
|
|
923
|
+
if (parsedResourceMetadataUrl.href.includes("#")) {
|
|
924
|
+
throw new McpNativeOAuthError("invalid-storage", "Stored protected-resource metadata URL must not contain a fragment");
|
|
925
|
+
}
|
|
926
|
+
resourceMetadataUrl = parsedResourceMetadataUrl.href;
|
|
927
|
+
}
|
|
928
|
+
return {
|
|
929
|
+
authorizationServerUrl,
|
|
930
|
+
...(authorizationServerMetadata === undefined ? {} : { authorizationServerMetadata }),
|
|
931
|
+
...(resourceMetadataResult?.success ? { resourceMetadata: resourceMetadataResult.data } : {}),
|
|
932
|
+
...(resourceMetadataUrl === undefined ? {} : { resourceMetadataUrl }),
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
function assertOAuthRecord(value, label) {
|
|
936
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
937
|
+
throw new McpNativeOAuthError("invalid-storage", `${label} must be a JSON object`);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
function assertBoundedOAuthJson(value, label, budget) {
|
|
941
|
+
const seen = new WeakSet();
|
|
942
|
+
const pending = [{ depth: 0, value }];
|
|
943
|
+
let arrayItems = 0;
|
|
944
|
+
let nodes = 0;
|
|
945
|
+
let properties = 0;
|
|
946
|
+
let stringCodeUnits = 0;
|
|
947
|
+
const countString = (text, limit) => {
|
|
948
|
+
if (text.length > limit) {
|
|
949
|
+
throw new McpNativeOAuthError("invalid-storage", `${label} contains a string that exceeds the supported size`);
|
|
950
|
+
}
|
|
951
|
+
stringCodeUnits += text.length;
|
|
952
|
+
if (stringCodeUnits > budget.maxTotalStringCodeUnits) {
|
|
953
|
+
throw new McpNativeOAuthError("invalid-storage", `${label} exceeds the cumulative supported string size`);
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
while (pending.length > 0) {
|
|
957
|
+
const entry = pending.pop();
|
|
958
|
+
nodes += 1;
|
|
959
|
+
if (nodes > budget.maxNodes || entry.depth > budget.maxDepth) {
|
|
960
|
+
throw new McpNativeOAuthError("invalid-storage", `${label} exceeds the supported structural complexity`);
|
|
961
|
+
}
|
|
962
|
+
if (entry.value === null)
|
|
963
|
+
continue;
|
|
964
|
+
if (entry.value === undefined) {
|
|
965
|
+
throw new McpNativeOAuthError("invalid-storage", `${label} contains a non-JSON value`);
|
|
966
|
+
}
|
|
967
|
+
if (typeof entry.value === "string") {
|
|
968
|
+
countString(entry.value, budget.maxStringCodeUnits);
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
if (typeof entry.value === "number") {
|
|
972
|
+
if (!Number.isFinite(entry.value)) {
|
|
973
|
+
throw new McpNativeOAuthError("invalid-storage", `${label} contains a non-JSON number`);
|
|
974
|
+
}
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
if (typeof entry.value === "boolean")
|
|
978
|
+
continue;
|
|
979
|
+
if (typeof entry.value !== "object") {
|
|
980
|
+
throw new McpNativeOAuthError("invalid-storage", `${label} contains a non-JSON value`);
|
|
981
|
+
}
|
|
982
|
+
if (seen.has(entry.value)) {
|
|
983
|
+
throw new McpNativeOAuthError("invalid-storage", `${label} contains a circular value`);
|
|
984
|
+
}
|
|
985
|
+
seen.add(entry.value);
|
|
986
|
+
if (Array.isArray(entry.value)) {
|
|
987
|
+
if (entry.value.length > budget.maxArrayItems) {
|
|
988
|
+
throw new McpNativeOAuthError("invalid-storage", `${label} contains an array that exceeds the supported size`);
|
|
989
|
+
}
|
|
990
|
+
arrayItems += entry.value.length;
|
|
991
|
+
if (arrayItems > budget.maxCumulativeArrayItems) {
|
|
992
|
+
throw new McpNativeOAuthError("invalid-storage", `${label} exceeds the cumulative supported array size`);
|
|
993
|
+
}
|
|
994
|
+
for (let index = 0; index < entry.value.length; index += 1) {
|
|
995
|
+
const descriptor = Object.getOwnPropertyDescriptor(entry.value, index);
|
|
996
|
+
if (descriptor === undefined || !("value" in descriptor)) {
|
|
997
|
+
throw new McpNativeOAuthError("invalid-storage", `${label} contains a non-JSON array`);
|
|
998
|
+
}
|
|
999
|
+
pending.push({ depth: entry.depth + 1, value: descriptor.value });
|
|
1000
|
+
}
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
const prototype = Object.getPrototypeOf(entry.value);
|
|
1004
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
1005
|
+
throw new McpNativeOAuthError("invalid-storage", `${label} contains a non-plain object`);
|
|
1006
|
+
}
|
|
1007
|
+
const keys = Reflect.ownKeys(entry.value);
|
|
1008
|
+
if (keys.length > budget.maxObjectProperties) {
|
|
1009
|
+
throw new McpNativeOAuthError("invalid-storage", `${label} contains an object that exceeds the supported size`);
|
|
1010
|
+
}
|
|
1011
|
+
properties += keys.length;
|
|
1012
|
+
if (properties > budget.maxCumulativeProperties) {
|
|
1013
|
+
throw new McpNativeOAuthError("invalid-storage", `${label} exceeds the cumulative supported property count`);
|
|
1014
|
+
}
|
|
1015
|
+
for (const key of keys) {
|
|
1016
|
+
if (typeof key !== "string") {
|
|
1017
|
+
throw new McpNativeOAuthError("invalid-storage", `${label} contains a non-JSON key`);
|
|
1018
|
+
}
|
|
1019
|
+
countString(key, budget.maxPropertyNameCodeUnits);
|
|
1020
|
+
const descriptor = Object.getOwnPropertyDescriptor(entry.value, key);
|
|
1021
|
+
if (!descriptor.enumerable || !("value" in descriptor)) {
|
|
1022
|
+
throw new McpNativeOAuthError("invalid-storage", `${label} contains a non-JSON property`);
|
|
1023
|
+
}
|
|
1024
|
+
if (descriptor.value === undefined &&
|
|
1025
|
+
entry.depth === 0 &&
|
|
1026
|
+
budget.allowedUndefinedRootProperties?.has(key)) {
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
pending.push({ depth: entry.depth + 1, value: descriptor.value });
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
function parseAuthorizationServerMetadata(value) {
|
|
1034
|
+
if (value === undefined) {
|
|
1035
|
+
return undefined;
|
|
1036
|
+
}
|
|
1037
|
+
const oauth = OAuthMetadataSchema.safeParse(value);
|
|
1038
|
+
if (oauth.success) {
|
|
1039
|
+
assertSecureAuthorizationServerMetadataEndpoints(oauth.data);
|
|
1040
|
+
return oauth.data;
|
|
1041
|
+
}
|
|
1042
|
+
const openId = OpenIdProviderDiscoveryMetadataSchema.safeParse(value);
|
|
1043
|
+
if (openId.success) {
|
|
1044
|
+
assertSecureAuthorizationServerMetadataEndpoints(openId.data);
|
|
1045
|
+
return openId.data;
|
|
1046
|
+
}
|
|
1047
|
+
throw new McpNativeOAuthError("invalid-storage", "Stored authorization-server metadata is invalid", { cause: openId.error });
|
|
1048
|
+
}
|
|
1049
|
+
function assertSecureAuthorizationServerMetadataEndpoints(metadata) {
|
|
1050
|
+
for (const [field, value] of Object.entries(metadata)) {
|
|
1051
|
+
if (typeof value !== "string" ||
|
|
1052
|
+
(field !== "service_documentation" && !field.endsWith("_endpoint") && !field.endsWith("_uri"))) {
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
const endpoint = parseStoredSecureEndpoint(value, `authorization-server metadata ${field}`);
|
|
1056
|
+
if (endpoint.href.includes("#")) {
|
|
1057
|
+
throw new McpNativeOAuthError("invalid-storage", `Stored authorization-server metadata ${field} must not contain a fragment`);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
function parseStoredSecureEndpoint(value, label) {
|
|
1062
|
+
try {
|
|
1063
|
+
return parseSecureEndpoint(value, label);
|
|
1064
|
+
}
|
|
1065
|
+
catch (error) {
|
|
1066
|
+
throw new McpNativeOAuthError("invalid-storage", `${label} is invalid`, { cause: error });
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
function assertCodeVerifier(verifier) {
|
|
1070
|
+
if (typeof verifier !== "string" || !PKCE_VERIFIER_PATTERN.test(verifier)) {
|
|
1071
|
+
throw new McpNativeOAuthError("invalid-storage", "OAuth PKCE verifier must contain 43 to 128 RFC 7636 unreserved characters");
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
function assertSecureStore(storage) {
|
|
1075
|
+
const methods = [
|
|
1076
|
+
"loadClientInformation",
|
|
1077
|
+
"saveClientInformation",
|
|
1078
|
+
"loadTokens",
|
|
1079
|
+
"saveTokens",
|
|
1080
|
+
"loadCodeVerifier",
|
|
1081
|
+
"saveCodeVerifier",
|
|
1082
|
+
"loadPendingAuthorization",
|
|
1083
|
+
"savePendingAuthorization",
|
|
1084
|
+
"reserveOAuthState",
|
|
1085
|
+
"saveOAuthState",
|
|
1086
|
+
"consumeOAuthState",
|
|
1087
|
+
"claimOAuthStateForCleanup",
|
|
1088
|
+
"clearOAuthState",
|
|
1089
|
+
"loadDiscoveryState",
|
|
1090
|
+
"saveDiscoveryState",
|
|
1091
|
+
"invalidate",
|
|
1092
|
+
];
|
|
1093
|
+
if (storage === null ||
|
|
1094
|
+
(typeof storage !== "object" && typeof storage !== "function") ||
|
|
1095
|
+
methods.some((method) => typeof storage[method] !== "function")) {
|
|
1096
|
+
throw new McpNativeOAuthError("invalid-configuration", "OAuth secure store must implement the complete persistence contract");
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
function assertScopeStore(storage) {
|
|
1100
|
+
if (storage === undefined)
|
|
1101
|
+
return;
|
|
1102
|
+
if (storage === null ||
|
|
1103
|
+
(typeof storage !== "object" && typeof storage !== "function") ||
|
|
1104
|
+
typeof storage.load !== "function" ||
|
|
1105
|
+
typeof storage.save !== "function" ||
|
|
1106
|
+
typeof storage.remove !== "function") {
|
|
1107
|
+
throw new McpNativeOAuthError("invalid-configuration", "OAuth scope store must implement load, save, and remove");
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
function parseCallbackUrl(value, redirectUrl) {
|
|
1111
|
+
const serialized = value instanceof URL ? value.href : value;
|
|
1112
|
+
if (serialized.length > MAX_CALLBACK_CODE_UNITS) {
|
|
1113
|
+
throw new McpNativeOAuthError("invalid-callback", "OAuth callback URL exceeds the supported size");
|
|
1114
|
+
}
|
|
1115
|
+
let callback;
|
|
1116
|
+
try {
|
|
1117
|
+
callback = new URL(serialized);
|
|
1118
|
+
}
|
|
1119
|
+
catch (error) {
|
|
1120
|
+
throw new McpNativeOAuthError("invalid-callback", "OAuth callback must be an absolute URL", {
|
|
1121
|
+
cause: error,
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
if (callback.href.includes("#")) {
|
|
1125
|
+
throw new McpNativeOAuthError("invalid-callback", "OAuth callback must not contain a fragment");
|
|
1126
|
+
}
|
|
1127
|
+
if (callback.protocol !== redirectUrl.protocol ||
|
|
1128
|
+
callback.username !== redirectUrl.username ||
|
|
1129
|
+
callback.password !== redirectUrl.password ||
|
|
1130
|
+
callback.host !== redirectUrl.host ||
|
|
1131
|
+
callback.pathname !== redirectUrl.pathname) {
|
|
1132
|
+
throw new McpNativeOAuthError("callback-mismatch", "OAuth callback does not match the configured redirect URL");
|
|
1133
|
+
}
|
|
1134
|
+
for (const [name, expected] of redirectUrl.searchParams) {
|
|
1135
|
+
const actual = callback.searchParams.getAll(name);
|
|
1136
|
+
if (actual.length !== 1 || actual[0] !== expected) {
|
|
1137
|
+
throw new McpNativeOAuthError("callback-mismatch", "OAuth callback changed a configured redirect parameter");
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
assertBoundedCallbackParameters(callback, redirectUrl);
|
|
1141
|
+
return callback;
|
|
1142
|
+
}
|
|
1143
|
+
function assertBoundedCallbackParameters(callback, redirectUrl) {
|
|
1144
|
+
const configuredNames = new Set(redirectUrl.searchParams.keys());
|
|
1145
|
+
const parameters = [...callback.searchParams];
|
|
1146
|
+
if (parameters.length > MAX_CALLBACK_PARAMETERS) {
|
|
1147
|
+
throw new McpNativeOAuthError("invalid-callback", "OAuth callback contains too many parameters");
|
|
1148
|
+
}
|
|
1149
|
+
for (const [name, value] of parameters) {
|
|
1150
|
+
if (!CALLBACK_PARAMETER_NAMES.has(name) && !configuredNames.has(name)) {
|
|
1151
|
+
throw new McpNativeOAuthError("invalid-callback", "OAuth callback contains an unsupported parameter");
|
|
1152
|
+
}
|
|
1153
|
+
const limit = CALLBACK_VALUE_LIMITS[name] ??
|
|
1154
|
+
MAX_CALLBACK_PARAMETER_VALUE_CODE_UNITS;
|
|
1155
|
+
if (name.length > MAX_CALLBACK_PARAMETER_NAME_CODE_UNITS || value.length > limit) {
|
|
1156
|
+
throw new McpNativeOAuthError("invalid-callback", "OAuth callback parameter exceeds the supported size");
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
function requireSingleParameter(parameters, name) {
|
|
1161
|
+
const values = parameters.getAll(name);
|
|
1162
|
+
if (values.length !== 1 || values[0] === "") {
|
|
1163
|
+
throw new McpNativeOAuthError("invalid-callback", `OAuth callback must contain exactly one non-empty ${name} parameter`);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
function parseNonCredentialHeaders(input) {
|
|
1167
|
+
if (input === undefined) {
|
|
1168
|
+
return undefined;
|
|
1169
|
+
}
|
|
1170
|
+
const entries = Array.isArray(input) ? input : Object.entries(input);
|
|
1171
|
+
const output = {};
|
|
1172
|
+
for (const [name, value] of entries) {
|
|
1173
|
+
const normalized = name.toLowerCase();
|
|
1174
|
+
if (normalized === "authorization" ||
|
|
1175
|
+
normalized === "cookie" ||
|
|
1176
|
+
normalized === "proxy-authorization") {
|
|
1177
|
+
throw new McpNativeOAuthError("invalid-configuration", `OAuth transport header ${name} may carry credentials outside the provider`);
|
|
1178
|
+
}
|
|
1179
|
+
output[name] = value;
|
|
1180
|
+
}
|
|
1181
|
+
return output;
|
|
1182
|
+
}
|
|
1183
|
+
//# sourceMappingURL=oauth.js.map
|