@ai-sdk/mcp 2.0.0-beta.6 → 2.0.0-beta.66
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/CHANGELOG.md +490 -8
- package/README.md +134 -0
- package/dist/index.d.ts +140 -2
- package/dist/index.js +750 -345
- package/dist/index.js.map +1 -1
- package/dist/mcp-stdio/index.d.ts +8 -0
- package/dist/mcp-stdio/index.js +170 -172
- package/dist/mcp-stdio/index.js.map +1 -1
- package/package.json +18 -19
- package/src/error/mcp-client-error.ts +40 -0
- package/src/index.ts +16 -1
- package/src/tool/index.ts +1 -0
- package/src/tool/json-rpc-message.ts +7 -0
- package/src/tool/mcp-apps.ts +254 -0
- package/src/tool/mcp-client.ts +128 -43
- package/src/tool/mcp-http-transport.ts +72 -24
- package/src/tool/mcp-sse-transport.ts +42 -16
- package/src/tool/mcp-stdio/create-child-process.ts +2 -2
- package/src/tool/mcp-stdio/mcp-stdio-transport.ts +17 -14
- package/src/tool/mcp-transport.ts +21 -3
- package/src/tool/mock-mcp-transport.ts +8 -9
- package/src/tool/oauth-types.ts +22 -18
- package/src/tool/oauth.ts +324 -37
- package/src/tool/types.ts +27 -3
- package/src/util/oauth-util.ts +13 -0
- package/dist/index.d.mts +0 -516
- package/dist/index.mjs +0 -2137
- package/dist/index.mjs.map +0 -1
- package/dist/mcp-stdio/index.d.mts +0 -89
- package/dist/mcp-stdio/index.mjs +0 -426
- package/dist/mcp-stdio/index.mjs.map +0 -1
package/src/tool/oauth.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import pkceChallenge from 'pkce-challenge';
|
|
2
2
|
import {
|
|
3
|
-
OAuthTokens,
|
|
4
|
-
OAuthProtectedResourceMetadata,
|
|
5
3
|
OAuthProtectedResourceMetadataSchema,
|
|
6
4
|
OAuthMetadataSchema,
|
|
7
5
|
OpenIdProviderDiscoveryMetadataSchema,
|
|
8
|
-
AuthorizationServerMetadata,
|
|
9
|
-
OAuthClientInformation,
|
|
10
6
|
OAuthTokensSchema,
|
|
11
7
|
OAuthErrorResponseSchema,
|
|
12
|
-
OAuthClientMetadata,
|
|
13
|
-
OAuthClientInformationFull,
|
|
14
8
|
OAuthClientInformationFullSchema,
|
|
9
|
+
type OAuthTokens,
|
|
10
|
+
type OAuthProtectedResourceMetadata,
|
|
11
|
+
type AuthorizationServerMetadata,
|
|
12
|
+
type OAuthClientInformation,
|
|
13
|
+
type OAuthClientMetadata,
|
|
14
|
+
type OAuthClientInformationFull,
|
|
15
15
|
} from './oauth-types';
|
|
16
16
|
import {
|
|
17
17
|
MCPClientOAuthError,
|
|
@@ -24,12 +24,17 @@ import {
|
|
|
24
24
|
import {
|
|
25
25
|
resourceUrlFromServerUrl,
|
|
26
26
|
checkResourceAllowed,
|
|
27
|
+
resourceUrlStripSlash,
|
|
27
28
|
} from '../util/oauth-util';
|
|
28
29
|
import { LATEST_PROTOCOL_VERSION } from './types';
|
|
29
|
-
import { FetchFunction } from '@ai-sdk/provider-utils';
|
|
30
|
-
|
|
30
|
+
import { parseJSON, type FetchFunction } from '@ai-sdk/provider-utils';
|
|
31
31
|
export type AuthResult = 'AUTHORIZED' | 'REDIRECT';
|
|
32
32
|
|
|
33
|
+
export interface OAuthAuthorizationServerInformation {
|
|
34
|
+
authorizationServerUrl: string;
|
|
35
|
+
tokenEndpoint: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
33
38
|
export interface OAuthClientProvider {
|
|
34
39
|
/**
|
|
35
40
|
* Returns current access token if present; undefined otherwise.
|
|
@@ -82,6 +87,21 @@ export interface OAuthClientProvider {
|
|
|
82
87
|
saveClientInformation?(
|
|
83
88
|
clientInformation: OAuthClientInformation,
|
|
84
89
|
): void | Promise<void>;
|
|
90
|
+
authorizationServerInformation?():
|
|
91
|
+
| OAuthAuthorizationServerInformation
|
|
92
|
+
| undefined
|
|
93
|
+
| Promise<OAuthAuthorizationServerInformation | undefined>;
|
|
94
|
+
saveAuthorizationServerInformation?(
|
|
95
|
+
authorizationServerInformation: OAuthAuthorizationServerInformation,
|
|
96
|
+
): void | Promise<void>;
|
|
97
|
+
/**
|
|
98
|
+
* Validates an authorization server URL discovered from MCP protected resource
|
|
99
|
+
* metadata before the client fetches its OAuth metadata.
|
|
100
|
+
*/
|
|
101
|
+
validateAuthorizationServerURL?(
|
|
102
|
+
serverUrl: string | URL,
|
|
103
|
+
authorizationServerUrl: string | URL,
|
|
104
|
+
): void | Promise<void>;
|
|
85
105
|
state?(): string | Promise<string>;
|
|
86
106
|
saveState?(state: string): void | Promise<void>;
|
|
87
107
|
storedState?(): string | undefined | Promise<string | undefined>;
|
|
@@ -98,6 +118,158 @@ export class UnauthorizedError extends Error {
|
|
|
98
118
|
}
|
|
99
119
|
}
|
|
100
120
|
|
|
121
|
+
function normalizeUrl(url: string | URL): string {
|
|
122
|
+
return new URL(url).href;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function createAuthorizationServerInformation(
|
|
126
|
+
authorizationServerUrl: string | URL,
|
|
127
|
+
metadata?: AuthorizationServerMetadata,
|
|
128
|
+
): OAuthAuthorizationServerInformation {
|
|
129
|
+
return {
|
|
130
|
+
authorizationServerUrl: normalizeUrl(authorizationServerUrl),
|
|
131
|
+
tokenEndpoint: normalizeUrl(
|
|
132
|
+
metadata?.token_endpoint
|
|
133
|
+
? new URL(metadata.token_endpoint)
|
|
134
|
+
: new URL('/token', authorizationServerUrl),
|
|
135
|
+
),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function addAuthorizationServerInformationToTokens(
|
|
140
|
+
tokens: OAuthTokens,
|
|
141
|
+
authorizationServerInformation: OAuthAuthorizationServerInformation,
|
|
142
|
+
): OAuthTokens {
|
|
143
|
+
return {
|
|
144
|
+
...tokens,
|
|
145
|
+
authorization_server: authorizationServerInformation.authorizationServerUrl,
|
|
146
|
+
token_endpoint: authorizationServerInformation.tokenEndpoint,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function addAuthorizationServerInformationToClientInformation<
|
|
151
|
+
CLIENT_INFORMATION extends OAuthClientInformation,
|
|
152
|
+
>(
|
|
153
|
+
clientInformation: CLIENT_INFORMATION,
|
|
154
|
+
authorizationServerInformation: OAuthAuthorizationServerInformation,
|
|
155
|
+
): CLIENT_INFORMATION {
|
|
156
|
+
return {
|
|
157
|
+
...clientInformation,
|
|
158
|
+
authorization_server: authorizationServerInformation.authorizationServerUrl,
|
|
159
|
+
token_endpoint: authorizationServerInformation.tokenEndpoint,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getAuthorizationServerInformationFromCredentials(credentials?: {
|
|
164
|
+
authorization_server?: string;
|
|
165
|
+
token_endpoint?: string;
|
|
166
|
+
}): OAuthAuthorizationServerInformation | undefined {
|
|
167
|
+
if (!credentials?.authorization_server || !credentials.token_endpoint) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
authorizationServerUrl: normalizeUrl(credentials.authorization_server),
|
|
173
|
+
tokenEndpoint: normalizeUrl(credentials.token_endpoint),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function getStoredAuthorizationServerInformation({
|
|
178
|
+
provider,
|
|
179
|
+
clientInformation,
|
|
180
|
+
tokens,
|
|
181
|
+
}: {
|
|
182
|
+
provider: OAuthClientProvider;
|
|
183
|
+
clientInformation: OAuthClientInformation;
|
|
184
|
+
tokens?: OAuthTokens;
|
|
185
|
+
}): Promise<OAuthAuthorizationServerInformation | undefined> {
|
|
186
|
+
const tokenAuthorizationServerInformation =
|
|
187
|
+
getAuthorizationServerInformationFromCredentials(tokens);
|
|
188
|
+
if (tokenAuthorizationServerInformation) {
|
|
189
|
+
return tokenAuthorizationServerInformation;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const providerAuthorizationServerInformation =
|
|
193
|
+
await provider.authorizationServerInformation?.();
|
|
194
|
+
if (providerAuthorizationServerInformation) {
|
|
195
|
+
return {
|
|
196
|
+
authorizationServerUrl: normalizeUrl(
|
|
197
|
+
providerAuthorizationServerInformation.authorizationServerUrl,
|
|
198
|
+
),
|
|
199
|
+
tokenEndpoint: normalizeUrl(
|
|
200
|
+
providerAuthorizationServerInformation.tokenEndpoint,
|
|
201
|
+
),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return getAuthorizationServerInformationFromCredentials(clientInformation);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function saveAuthorizationServerInformation({
|
|
209
|
+
provider,
|
|
210
|
+
clientInformation,
|
|
211
|
+
authorizationServerInformation,
|
|
212
|
+
}: {
|
|
213
|
+
provider: OAuthClientProvider;
|
|
214
|
+
clientInformation: OAuthClientInformation;
|
|
215
|
+
authorizationServerInformation: OAuthAuthorizationServerInformation;
|
|
216
|
+
}): Promise<boolean> {
|
|
217
|
+
if (provider.saveAuthorizationServerInformation) {
|
|
218
|
+
await provider.saveAuthorizationServerInformation(
|
|
219
|
+
authorizationServerInformation,
|
|
220
|
+
);
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (provider.saveClientInformation) {
|
|
225
|
+
await provider.saveClientInformation(
|
|
226
|
+
addAuthorizationServerInformationToClientInformation(
|
|
227
|
+
clientInformation,
|
|
228
|
+
authorizationServerInformation,
|
|
229
|
+
),
|
|
230
|
+
);
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function assertResourceMetadataUrlSameOrigin(
|
|
238
|
+
serverUrl: string | URL,
|
|
239
|
+
resourceMetadataUrl?: URL,
|
|
240
|
+
): void {
|
|
241
|
+
if (!resourceMetadataUrl) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const expectedOrigin = new URL(serverUrl).origin;
|
|
246
|
+
if (resourceMetadataUrl.origin !== expectedOrigin) {
|
|
247
|
+
throw new MCPClientOAuthError({
|
|
248
|
+
message: `OAuth protected resource metadata URL ${resourceMetadataUrl.href} must have the same origin as the MCP server URL ${expectedOrigin}`,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function assertAuthorizationServerInformationMatches({
|
|
254
|
+
storedAuthorizationServerInformation,
|
|
255
|
+
currentAuthorizationServerInformation,
|
|
256
|
+
}: {
|
|
257
|
+
storedAuthorizationServerInformation: OAuthAuthorizationServerInformation;
|
|
258
|
+
currentAuthorizationServerInformation: OAuthAuthorizationServerInformation;
|
|
259
|
+
}): void {
|
|
260
|
+
if (
|
|
261
|
+
storedAuthorizationServerInformation.authorizationServerUrl !==
|
|
262
|
+
currentAuthorizationServerInformation.authorizationServerUrl ||
|
|
263
|
+
storedAuthorizationServerInformation.tokenEndpoint !==
|
|
264
|
+
currentAuthorizationServerInformation.tokenEndpoint
|
|
265
|
+
) {
|
|
266
|
+
throw new MCPClientOAuthError({
|
|
267
|
+
message:
|
|
268
|
+
'OAuth authorization server metadata does not match the metadata that issued the stored credentials',
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
101
273
|
/**
|
|
102
274
|
* Extracts the OAuth 2.0 Protected Resource Metadata URL from a WWW-Authenticate header (RFC9728).
|
|
103
275
|
* Looks for a resource="..." parameter.
|
|
@@ -270,23 +442,30 @@ export async function discoverOAuthProtectedResourceMetadata(
|
|
|
270
442
|
*/
|
|
271
443
|
export function buildDiscoveryUrls(
|
|
272
444
|
authorizationServerUrl: string | URL,
|
|
273
|
-
): { url: URL; type: 'oauth' | 'oidc' }[] {
|
|
445
|
+
): { url: URL; type: 'oauth' | 'oidc'; expectedIssuer: string }[] {
|
|
274
446
|
const url =
|
|
275
447
|
typeof authorizationServerUrl === 'string'
|
|
276
448
|
? new URL(authorizationServerUrl)
|
|
277
449
|
: authorizationServerUrl;
|
|
278
450
|
const hasPath = url.pathname !== '/';
|
|
279
|
-
const
|
|
451
|
+
const rootIssuer = url.origin;
|
|
452
|
+
const urlsToTry: {
|
|
453
|
+
url: URL;
|
|
454
|
+
type: 'oauth' | 'oidc';
|
|
455
|
+
expectedIssuer: string;
|
|
456
|
+
}[] = [];
|
|
280
457
|
|
|
281
458
|
if (!hasPath) {
|
|
282
459
|
urlsToTry.push({
|
|
283
460
|
url: new URL('/.well-known/oauth-authorization-server', url.origin),
|
|
284
461
|
type: 'oauth',
|
|
462
|
+
expectedIssuer: rootIssuer,
|
|
285
463
|
});
|
|
286
464
|
|
|
287
465
|
urlsToTry.push({
|
|
288
466
|
url: new URL('/.well-known/openid-configuration', url.origin),
|
|
289
467
|
type: 'oidc',
|
|
468
|
+
expectedIssuer: rootIssuer,
|
|
290
469
|
});
|
|
291
470
|
|
|
292
471
|
return urlsToTry;
|
|
@@ -296,6 +475,7 @@ export function buildDiscoveryUrls(
|
|
|
296
475
|
if (pathname.endsWith('/')) {
|
|
297
476
|
pathname = pathname.slice(0, -1);
|
|
298
477
|
}
|
|
478
|
+
const pathIssuer = `${url.origin}${pathname}`;
|
|
299
479
|
|
|
300
480
|
urlsToTry.push({
|
|
301
481
|
url: new URL(
|
|
@@ -303,26 +483,41 @@ export function buildDiscoveryUrls(
|
|
|
303
483
|
url.origin,
|
|
304
484
|
),
|
|
305
485
|
type: 'oauth',
|
|
486
|
+
expectedIssuer: pathIssuer,
|
|
306
487
|
});
|
|
307
488
|
|
|
308
489
|
urlsToTry.push({
|
|
309
490
|
url: new URL('/.well-known/oauth-authorization-server', url.origin),
|
|
310
491
|
type: 'oauth',
|
|
492
|
+
expectedIssuer: rootIssuer,
|
|
311
493
|
});
|
|
312
494
|
|
|
313
495
|
urlsToTry.push({
|
|
314
496
|
url: new URL(`/.well-known/openid-configuration${pathname}`, url.origin),
|
|
315
497
|
type: 'oidc',
|
|
498
|
+
expectedIssuer: pathIssuer,
|
|
316
499
|
});
|
|
317
500
|
|
|
318
501
|
urlsToTry.push({
|
|
319
502
|
url: new URL(`${pathname}/.well-known/openid-configuration`, url.origin),
|
|
320
503
|
type: 'oidc',
|
|
504
|
+
expectedIssuer: pathIssuer,
|
|
321
505
|
});
|
|
322
506
|
|
|
323
507
|
return urlsToTry;
|
|
324
508
|
}
|
|
325
509
|
|
|
510
|
+
function assertMetadataIssuerMatches(
|
|
511
|
+
metadata: AuthorizationServerMetadata,
|
|
512
|
+
expectedIssuer: string,
|
|
513
|
+
): void {
|
|
514
|
+
if (metadata.issuer !== expectedIssuer) {
|
|
515
|
+
throw new MCPClientOAuthError({
|
|
516
|
+
message: `OAuth authorization server metadata issuer ${metadata.issuer} does not match expected issuer ${expectedIssuer}`,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
326
521
|
export async function discoverAuthorizationServerMetadata(
|
|
327
522
|
authorizationServerUrl: string | URL,
|
|
328
523
|
{
|
|
@@ -337,7 +532,7 @@ export async function discoverAuthorizationServerMetadata(
|
|
|
337
532
|
|
|
338
533
|
const urlsToTry = buildDiscoveryUrls(authorizationServerUrl);
|
|
339
534
|
|
|
340
|
-
for (const { url: endpointUrl, type } of urlsToTry) {
|
|
535
|
+
for (const { url: endpointUrl, type, expectedIssuer } of urlsToTry) {
|
|
341
536
|
const response = await fetchWithCorsRetry(endpointUrl, headers, fetchFn);
|
|
342
537
|
|
|
343
538
|
if (!response) {
|
|
@@ -359,11 +554,14 @@ export async function discoverAuthorizationServerMetadata(
|
|
|
359
554
|
}
|
|
360
555
|
|
|
361
556
|
if (type === 'oauth') {
|
|
362
|
-
|
|
557
|
+
const metadata = OAuthMetadataSchema.parse(await response.json());
|
|
558
|
+
assertMetadataIssuerMatches(metadata, expectedIssuer);
|
|
559
|
+
return metadata;
|
|
363
560
|
} else {
|
|
364
561
|
const metadata = OpenIdProviderDiscoveryMetadataSchema.parse(
|
|
365
562
|
await response.json(),
|
|
366
563
|
);
|
|
564
|
+
assertMetadataIssuerMatches(metadata, expectedIssuer);
|
|
367
565
|
|
|
368
566
|
// MCP spec requires OIDC providers to support S256 PKCE
|
|
369
567
|
if (!metadata.code_challenge_methods_supported?.includes('S256')) {
|
|
@@ -451,7 +649,10 @@ export async function startAuthorization(
|
|
|
451
649
|
}
|
|
452
650
|
|
|
453
651
|
if (resource) {
|
|
454
|
-
authorizationUrl.searchParams.set(
|
|
652
|
+
authorizationUrl.searchParams.set(
|
|
653
|
+
'resource',
|
|
654
|
+
resourceUrlStripSlash(resource),
|
|
655
|
+
);
|
|
455
656
|
}
|
|
456
657
|
|
|
457
658
|
return { authorizationUrl, codeVerifier };
|
|
@@ -587,7 +788,9 @@ export async function parseErrorResponse(
|
|
|
587
788
|
const body = input instanceof Response ? await input.text() : input;
|
|
588
789
|
|
|
589
790
|
try {
|
|
590
|
-
const result = OAuthErrorResponseSchema.parse(
|
|
791
|
+
const result = OAuthErrorResponseSchema.parse(
|
|
792
|
+
await parseJSON({ text: body }),
|
|
793
|
+
);
|
|
591
794
|
const { error, error_description, error_uri } = result;
|
|
592
795
|
const errorClass = OAUTH_ERRORS[error] || ServerError;
|
|
593
796
|
return new errorClass({
|
|
@@ -662,7 +865,12 @@ export async function exchangeAuthorization(
|
|
|
662
865
|
});
|
|
663
866
|
|
|
664
867
|
if (addClientAuthentication) {
|
|
665
|
-
addClientAuthentication(
|
|
868
|
+
await addClientAuthentication(
|
|
869
|
+
headers,
|
|
870
|
+
params,
|
|
871
|
+
authorizationServerUrl,
|
|
872
|
+
metadata,
|
|
873
|
+
);
|
|
666
874
|
} else {
|
|
667
875
|
const supportedMethods =
|
|
668
876
|
metadata?.token_endpoint_auth_methods_supported ?? [];
|
|
@@ -675,7 +883,7 @@ export async function exchangeAuthorization(
|
|
|
675
883
|
}
|
|
676
884
|
|
|
677
885
|
if (resource) {
|
|
678
|
-
params.set('resource', resource
|
|
886
|
+
params.set('resource', resourceUrlStripSlash(resource));
|
|
679
887
|
}
|
|
680
888
|
|
|
681
889
|
const response = await (fetchFn ?? fetch)(tokenUrl, {
|
|
@@ -749,7 +957,12 @@ export async function refreshAuthorization(
|
|
|
749
957
|
});
|
|
750
958
|
|
|
751
959
|
if (addClientAuthentication) {
|
|
752
|
-
addClientAuthentication(
|
|
960
|
+
await addClientAuthentication(
|
|
961
|
+
headers,
|
|
962
|
+
params,
|
|
963
|
+
authorizationServerUrl,
|
|
964
|
+
metadata,
|
|
965
|
+
);
|
|
753
966
|
} else {
|
|
754
967
|
const supportedMethods =
|
|
755
968
|
metadata?.token_endpoint_auth_methods_supported ?? [];
|
|
@@ -762,7 +975,7 @@ export async function refreshAuthorization(
|
|
|
762
975
|
}
|
|
763
976
|
|
|
764
977
|
if (resource) {
|
|
765
|
-
params.set('resource', resource
|
|
978
|
+
params.set('resource', resourceUrlStripSlash(resource));
|
|
766
979
|
}
|
|
767
980
|
|
|
768
981
|
const response = await (fetchFn ?? fetch)(tokenUrl, {
|
|
@@ -904,6 +1117,11 @@ async function authInternal(
|
|
|
904
1117
|
): Promise<AuthResult> {
|
|
905
1118
|
let resourceMetadata: OAuthProtectedResourceMetadata | undefined;
|
|
906
1119
|
let authorizationServerUrl: string | URL | undefined;
|
|
1120
|
+
|
|
1121
|
+
/** Reject Protected Resource Metadata URLs outside the configured MCP server origin. */
|
|
1122
|
+
assertResourceMetadataUrlSameOrigin(serverUrl, resourceMetadataUrl);
|
|
1123
|
+
|
|
1124
|
+
/** Discover PRM and select its advertised authorization server. */
|
|
907
1125
|
try {
|
|
908
1126
|
resourceMetadata = await discoverOAuthProtectedResourceMetadata(
|
|
909
1127
|
serverUrl,
|
|
@@ -918,27 +1136,35 @@ async function authInternal(
|
|
|
918
1136
|
}
|
|
919
1137
|
} catch {}
|
|
920
1138
|
|
|
921
|
-
/**
|
|
922
|
-
* If we don't get a valid authorization server metadata from protected resource metadata,
|
|
923
|
-
* fallback to the legacy MCP spec's implementation (version 2025-03-26): MCP server acts as the Authorization server.
|
|
924
|
-
*/
|
|
1139
|
+
/** Fall back to legacy MCP behavior where the MCP server is the Authorization Server */
|
|
925
1140
|
if (!authorizationServerUrl) {
|
|
926
1141
|
authorizationServerUrl = serverUrl;
|
|
927
1142
|
}
|
|
928
1143
|
|
|
1144
|
+
/** Validate and select the resource value sent to the AS */
|
|
929
1145
|
const resource: URL | undefined = await selectResourceURL(
|
|
930
1146
|
serverUrl,
|
|
931
1147
|
provider,
|
|
932
1148
|
resourceMetadata,
|
|
933
1149
|
);
|
|
934
1150
|
|
|
1151
|
+
/** Let applications constrain discovered AS URLs before metadata fetches. */
|
|
1152
|
+
await provider.validateAuthorizationServerURL?.(
|
|
1153
|
+
serverUrl,
|
|
1154
|
+
authorizationServerUrl,
|
|
1155
|
+
);
|
|
1156
|
+
|
|
1157
|
+
/** Discover AS metadata and derive the credential pin for this flow */
|
|
935
1158
|
const metadata = await discoverAuthorizationServerMetadata(
|
|
936
1159
|
authorizationServerUrl,
|
|
937
1160
|
{
|
|
938
1161
|
fetchFn,
|
|
939
1162
|
},
|
|
940
1163
|
);
|
|
1164
|
+
const currentAuthorizationServerInformation =
|
|
1165
|
+
createAuthorizationServerInformation(authorizationServerUrl, metadata);
|
|
941
1166
|
|
|
1167
|
+
/** Load or register client credentials with the AS pin attached. */
|
|
942
1168
|
let clientInformation = await Promise.resolve(provider.clientInformation());
|
|
943
1169
|
if (!clientInformation) {
|
|
944
1170
|
if (authorizationCode !== undefined) {
|
|
@@ -959,11 +1185,14 @@ async function authInternal(
|
|
|
959
1185
|
fetchFn,
|
|
960
1186
|
});
|
|
961
1187
|
|
|
962
|
-
|
|
963
|
-
|
|
1188
|
+
clientInformation = addAuthorizationServerInformationToClientInformation(
|
|
1189
|
+
fullInformation,
|
|
1190
|
+
currentAuthorizationServerInformation,
|
|
1191
|
+
);
|
|
1192
|
+
await provider.saveClientInformation(clientInformation);
|
|
964
1193
|
}
|
|
965
1194
|
|
|
966
|
-
|
|
1195
|
+
/** On callback, validate state and AS pin before code exchange */
|
|
967
1196
|
if (authorizationCode !== undefined) {
|
|
968
1197
|
if (provider.storedState) {
|
|
969
1198
|
const expectedState = await provider.storedState();
|
|
@@ -974,6 +1203,22 @@ async function authInternal(
|
|
|
974
1203
|
}
|
|
975
1204
|
}
|
|
976
1205
|
|
|
1206
|
+
const storedAuthorizationServerInformation =
|
|
1207
|
+
await getStoredAuthorizationServerInformation({
|
|
1208
|
+
provider,
|
|
1209
|
+
clientInformation,
|
|
1210
|
+
});
|
|
1211
|
+
if (!storedAuthorizationServerInformation) {
|
|
1212
|
+
throw new MCPClientOAuthError({
|
|
1213
|
+
message:
|
|
1214
|
+
'Stored OAuth authorization server metadata is required when exchanging an authorization code',
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
assertAuthorizationServerInformationMatches({
|
|
1218
|
+
storedAuthorizationServerInformation,
|
|
1219
|
+
currentAuthorizationServerInformation,
|
|
1220
|
+
});
|
|
1221
|
+
|
|
977
1222
|
const codeVerifier = await provider.codeVerifier();
|
|
978
1223
|
const tokens = await exchangeAuthorization(authorizationServerUrl, {
|
|
979
1224
|
metadata,
|
|
@@ -986,27 +1231,55 @@ async function authInternal(
|
|
|
986
1231
|
fetchFn: fetchFn,
|
|
987
1232
|
});
|
|
988
1233
|
|
|
989
|
-
await provider.saveTokens(
|
|
1234
|
+
await provider.saveTokens(
|
|
1235
|
+
addAuthorizationServerInformationToTokens(
|
|
1236
|
+
tokens,
|
|
1237
|
+
currentAuthorizationServerInformation,
|
|
1238
|
+
),
|
|
1239
|
+
);
|
|
990
1240
|
return 'AUTHORIZED';
|
|
991
1241
|
}
|
|
992
1242
|
|
|
993
1243
|
const tokens = await provider.tokens();
|
|
994
1244
|
|
|
995
|
-
|
|
1245
|
+
/** Refresh only when stored credentials match the current AS pin */
|
|
996
1246
|
if (tokens?.refresh_token) {
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
metadata,
|
|
1247
|
+
const storedAuthorizationServerInformation =
|
|
1248
|
+
await getStoredAuthorizationServerInformation({
|
|
1249
|
+
provider,
|
|
1001
1250
|
clientInformation,
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1251
|
+
tokens,
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
if (storedAuthorizationServerInformation) {
|
|
1255
|
+
assertAuthorizationServerInformationMatches({
|
|
1256
|
+
storedAuthorizationServerInformation,
|
|
1257
|
+
currentAuthorizationServerInformation,
|
|
1006
1258
|
});
|
|
1259
|
+
} else {
|
|
1260
|
+
await provider.invalidateCredentials?.('tokens');
|
|
1261
|
+
}
|
|
1007
1262
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1263
|
+
try {
|
|
1264
|
+
if (storedAuthorizationServerInformation) {
|
|
1265
|
+
// Attempt to refresh the token
|
|
1266
|
+
const newTokens = await refreshAuthorization(authorizationServerUrl, {
|
|
1267
|
+
metadata,
|
|
1268
|
+
clientInformation,
|
|
1269
|
+
refreshToken: tokens.refresh_token,
|
|
1270
|
+
resource,
|
|
1271
|
+
addClientAuthentication: provider.addClientAuthentication,
|
|
1272
|
+
fetchFn,
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
await provider.saveTokens(
|
|
1276
|
+
addAuthorizationServerInformationToTokens(
|
|
1277
|
+
newTokens,
|
|
1278
|
+
currentAuthorizationServerInformation,
|
|
1279
|
+
),
|
|
1280
|
+
);
|
|
1281
|
+
return 'AUTHORIZED';
|
|
1282
|
+
}
|
|
1010
1283
|
} catch (error) {
|
|
1011
1284
|
if (
|
|
1012
1285
|
// If this is a ServerError, or an unknown type, log it out and try to continue. Otherwise, escalate so we can fix things and retry.
|
|
@@ -1021,6 +1294,7 @@ async function authInternal(
|
|
|
1021
1294
|
}
|
|
1022
1295
|
}
|
|
1023
1296
|
|
|
1297
|
+
/** Start authorization and persist the AS pin before redirecting */
|
|
1024
1298
|
const state = provider.state ? await provider.state() : undefined;
|
|
1025
1299
|
if (state && provider.saveState) {
|
|
1026
1300
|
await provider.saveState(state);
|
|
@@ -1039,6 +1313,19 @@ async function authInternal(
|
|
|
1039
1313
|
},
|
|
1040
1314
|
);
|
|
1041
1315
|
|
|
1316
|
+
const savedAuthorizationServerInformation =
|
|
1317
|
+
await saveAuthorizationServerInformation({
|
|
1318
|
+
provider,
|
|
1319
|
+
clientInformation,
|
|
1320
|
+
authorizationServerInformation: currentAuthorizationServerInformation,
|
|
1321
|
+
});
|
|
1322
|
+
if (!savedAuthorizationServerInformation) {
|
|
1323
|
+
throw new MCPClientOAuthError({
|
|
1324
|
+
message:
|
|
1325
|
+
'OAuth authorization server metadata must be saveable before starting authorization',
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1042
1329
|
await provider.saveCodeVerifier(codeVerifier);
|
|
1043
1330
|
await provider.redirectToAuthorization(authorizationUrl);
|
|
1044
1331
|
return 'REDIRECT';
|
package/src/tool/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod/v4';
|
|
2
|
-
import { JSONObject } from '@ai-sdk/provider';
|
|
3
|
-
import { FlexibleSchema, Tool } from '@ai-sdk/provider-utils';
|
|
2
|
+
import type { JSONObject } from '@ai-sdk/provider';
|
|
3
|
+
import type { FlexibleSchema, Tool } from '@ai-sdk/provider-utils';
|
|
4
4
|
|
|
5
5
|
export const LATEST_PROTOCOL_VERSION = '2025-11-25';
|
|
6
6
|
export const SUPPORTED_PROTOCOL_VERSIONS = [
|
|
@@ -10,6 +10,13 @@ export const SUPPORTED_PROTOCOL_VERSIONS = [
|
|
|
10
10
|
'2024-11-05',
|
|
11
11
|
];
|
|
12
12
|
|
|
13
|
+
export type McpProviderMetadata = {
|
|
14
|
+
clientName?: string;
|
|
15
|
+
title?: string;
|
|
16
|
+
toolName?: string;
|
|
17
|
+
app?: JSONObject;
|
|
18
|
+
};
|
|
19
|
+
|
|
13
20
|
/** MCP tool metadata - keys should follow MCP _meta key format specification */
|
|
14
21
|
const ToolMetaSchema = z.optional(z.record(z.string(), z.unknown()));
|
|
15
22
|
export type ToolMeta = z.infer<typeof ToolMetaSchema>;
|
|
@@ -56,8 +63,10 @@ export type McpToolSet<TOOL_SCHEMAS extends ToolSchemas = 'automatic'> =
|
|
|
56
63
|
const ClientOrServerImplementationSchema = z.looseObject({
|
|
57
64
|
name: z.string(),
|
|
58
65
|
version: z.string(),
|
|
66
|
+
title: z.optional(z.string()),
|
|
59
67
|
});
|
|
60
68
|
|
|
69
|
+
// Maps to `Implementation` in the MCP specification
|
|
61
70
|
export type Configuration = z.infer<typeof ClientOrServerImplementationSchema>;
|
|
62
71
|
|
|
63
72
|
export const BaseParamsSchema = z.looseObject({
|
|
@@ -232,10 +241,24 @@ const EmbeddedResourceSchema = z
|
|
|
232
241
|
resource: z.union([TextResourceContentsSchema, BlobResourceContentsSchema]),
|
|
233
242
|
})
|
|
234
243
|
.loose();
|
|
244
|
+
const ResourceLinkContentSchema = z
|
|
245
|
+
.object({
|
|
246
|
+
type: z.literal('resource_link'),
|
|
247
|
+
uri: z.string(),
|
|
248
|
+
name: z.string(),
|
|
249
|
+
description: z.optional(z.string()),
|
|
250
|
+
mimeType: z.optional(z.string()),
|
|
251
|
+
})
|
|
252
|
+
.loose();
|
|
235
253
|
|
|
236
254
|
export const CallToolResultSchema = ResultSchema.extend({
|
|
237
255
|
content: z.array(
|
|
238
|
-
z.union([
|
|
256
|
+
z.union([
|
|
257
|
+
TextContentSchema,
|
|
258
|
+
ImageContentSchema,
|
|
259
|
+
EmbeddedResourceSchema,
|
|
260
|
+
ResourceLinkContentSchema,
|
|
261
|
+
]),
|
|
239
262
|
),
|
|
240
263
|
/**
|
|
241
264
|
* @see https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content
|
|
@@ -304,6 +327,7 @@ const PromptMessageSchema = z
|
|
|
304
327
|
TextContentSchema,
|
|
305
328
|
ImageContentSchema,
|
|
306
329
|
EmbeddedResourceSchema,
|
|
330
|
+
ResourceLinkContentSchema,
|
|
307
331
|
]),
|
|
308
332
|
})
|
|
309
333
|
.loose();
|
package/src/util/oauth-util.ts
CHANGED
|
@@ -14,6 +14,19 @@ export function resourceUrlFromServerUrl(url: URL | string): URL {
|
|
|
14
14
|
return resourceURL;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Serializes a resource URL to a string, removing the trailing slash that
|
|
19
|
+
* URL.href adds to pathless URLs. Per the MCP spec, implementations SHOULD
|
|
20
|
+
* use the form without the trailing slash for better interoperability.
|
|
21
|
+
*/
|
|
22
|
+
export function resourceUrlStripSlash(resource: URL): string {
|
|
23
|
+
const href = resource.href;
|
|
24
|
+
if (resource.pathname === '/' && href.endsWith('/')) {
|
|
25
|
+
return href.slice(0, -1);
|
|
26
|
+
}
|
|
27
|
+
return href;
|
|
28
|
+
}
|
|
29
|
+
|
|
17
30
|
/**
|
|
18
31
|
* Checks if a requested resource URL matches a configured resource URL.
|
|
19
32
|
* A requested resource matches if it has the same scheme, domain, port,
|