Skip to content

Commit 0ce2da8

Browse files
author
itsuki
committed
add private variable to keep track of
authorization server url to avoid fetching Protected Resource Metadata every time
1 parent b90d5ba commit 0ce2da8

File tree

2 files changed

+42
-17
lines changed

2 files changed

+42
-17
lines changed

src/client/sse.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { EventSource, type ErrorEvent, type EventSourceInit } from "eventsource";
22
import { Transport } from "../shared/transport.js";
33
import { JSONRPCMessage, JSONRPCMessageSchema } from "../types.js";
4-
import { auth, AuthResult, OAuthClientProvider, UnauthorizedError } from "./auth.js";
4+
import { auth, AuthResult, discoverOAuthProtectedResourceMetadata, extractResourceMetadataUrl, OAuthClientProvider, UnauthorizedError } from "./auth.js";
55

66
export class SseError extends Error {
77
constructor(
@@ -19,23 +19,23 @@ export class SseError extends Error {
1919
export type SSEClientTransportOptions = {
2020
/**
2121
* An OAuth client provider to use for authentication.
22-
*
22+
*
2323
* When an `authProvider` is specified and the SSE connection is started:
2424
* 1. The connection is attempted with any existing access token from the `authProvider`.
2525
* 2. If the access token has expired, the `authProvider` is used to refresh the token.
2626
* 3. If token refresh fails or no access token exists, and auth is required, `OAuthClientProvider.redirectToAuthorization` is called, and an `UnauthorizedError` will be thrown from `connect`/`start`.
27-
*
27+
*
2828
* After the user has finished authorizing via their user agent, and is redirected back to the MCP client application, call `SSEClientTransport.finishAuth` with the authorization code before retrying the connection.
29-
*
29+
*
3030
* If an `authProvider` is not provided, and auth is required, an `UnauthorizedError` will be thrown.
31-
*
31+
*
3232
* `UnauthorizedError` might also be thrown when sending any message over the SSE transport, indicating that the session has expired, and needs to be re-authed and reconnected.
3333
*/
3434
authProvider?: OAuthClientProvider;
3535

3636
/**
3737
* Customizes the initial SSE request to the server (the request that begins the stream).
38-
*
38+
*
3939
* NOTE: Setting this property will prevent an `Authorization` header from
4040
* being automatically attached to the SSE request, if an `authProvider` is
4141
* also given. This can be worked around by setting the `Authorization` header
@@ -58,6 +58,7 @@ export class SSEClientTransport implements Transport {
5858
private _endpoint?: URL;
5959
private _abortController?: AbortController;
6060
private _url: URL;
61+
private _authServerUrl: URL | undefined;
6162
private _eventSourceInit?: EventSourceInit;
6263
private _requestInit?: RequestInit;
6364
private _authProvider?: OAuthClientProvider;
@@ -71,6 +72,7 @@ export class SSEClientTransport implements Transport {
7172
opts?: SSEClientTransportOptions,
7273
) {
7374
this._url = url;
75+
this._authServerUrl = undefined;
7476
this._eventSourceInit = opts?.eventSourceInit;
7577
this._requestInit = opts?.requestInit;
7678
this._authProvider = opts?.authProvider;
@@ -83,7 +85,7 @@ export class SSEClientTransport implements Transport {
8385

8486
let result: AuthResult;
8587
try {
86-
result = await auth(this._authProvider, { serverUrl: this._url });
88+
result = await auth(this._authProvider, { resourceServerUrl: this._url, authServerUrl: this._authServerUrl });
8789
} catch (error) {
8890
this.onerror?.(error as Error);
8991
throw error;
@@ -193,7 +195,7 @@ export class SSEClientTransport implements Transport {
193195
throw new UnauthorizedError("No auth provider");
194196
}
195197

196-
const result = await auth(this._authProvider, { serverUrl: this._url, authorizationCode });
198+
const result = await auth(this._authProvider, { resourceServerUrl: this._url, authorizationCode, authServerUrl: this._authServerUrl });
197199
if (result !== "AUTHORIZED") {
198200
throw new UnauthorizedError("Failed to authorize");
199201
}
@@ -225,7 +227,18 @@ export class SSEClientTransport implements Transport {
225227
const response = await fetch(this._endpoint, init);
226228
if (!response.ok) {
227229
if (response.status === 401 && this._authProvider) {
228-
const result = await auth(this._authProvider, { serverUrl: this._url });
230+
231+
const resourceMetadataUrl = extractResourceMetadataUrl(response);
232+
const protectedResourceMetadata = await discoverOAuthProtectedResourceMetadata(this._url, {
233+
resourceMetadataUrl: resourceMetadataUrl
234+
})
235+
if (protectedResourceMetadata.authorization_servers === undefined || protectedResourceMetadata.authorization_servers.length === 0) {
236+
throw new Error("Server does not speicify any authorization servers.");
237+
}
238+
this._authServerUrl = new URL(protectedResourceMetadata.authorization_servers[0]);
239+
240+
const result = await auth(this._authProvider, { resourceServerUrl: this._url, authServerUrl: this._authServerUrl });
241+
229242
if (result !== "AUTHORIZED") {
230243
throw new UnauthorizedError();
231244
}

src/client/streamableHttp.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Transport } from "../shared/transport.js";
22
import { isInitializedNotification, isJSONRPCRequest, isJSONRPCResponse, JSONRPCMessage, JSONRPCMessageSchema } from "../types.js";
3-
import { auth, AuthResult, OAuthClientProvider, UnauthorizedError } from "./auth.js";
3+
import { auth, AuthResult, discoverOAuthProtectedResourceMetadata, extractResourceMetadataUrl, OAuthClientProvider, UnauthorizedError } from "./auth.js";
44
import { EventSourceParserStream } from "eventsource-parser/stream";
55

66
// Default reconnection options for StreamableHTTP connections
@@ -119,6 +119,7 @@ export type StreamableHTTPClientTransportOptions = {
119119
export class StreamableHTTPClientTransport implements Transport {
120120
private _abortController?: AbortController;
121121
private _url: URL;
122+
private _authServerUrl: URL | undefined;
122123
private _requestInit?: RequestInit;
123124
private _authProvider?: OAuthClientProvider;
124125
private _sessionId?: string;
@@ -133,6 +134,7 @@ export class StreamableHTTPClientTransport implements Transport {
133134
opts?: StreamableHTTPClientTransportOptions,
134135
) {
135136
this._url = url;
137+
this._authServerUrl = undefined;
136138
this._requestInit = opts?.requestInit;
137139
this._authProvider = opts?.authProvider;
138140
this._sessionId = opts?.sessionId;
@@ -146,7 +148,7 @@ export class StreamableHTTPClientTransport implements Transport {
146148

147149
let result: AuthResult;
148150
try {
149-
result = await auth(this._authProvider, { serverUrl: this._url });
151+
result = await auth(this._authProvider, { resourceServerUrl: this._url, authServerUrl: this._authServerUrl });
150152
} catch (error) {
151153
this.onerror?.(error as Error);
152154
throw error;
@@ -225,7 +227,7 @@ export class StreamableHTTPClientTransport implements Transport {
225227

226228
/**
227229
* Calculates the next reconnection delay using backoff algorithm
228-
*
230+
*
229231
* @param attempt Current reconnection attempt count for the specific stream
230232
* @returns Time to wait in milliseconds before next reconnection attempt
231233
*/
@@ -242,7 +244,7 @@ export class StreamableHTTPClientTransport implements Transport {
242244

243245
/**
244246
* Schedule a reconnection attempt with exponential backoff
245-
*
247+
*
246248
* @param lastEventId The ID of the last received event for resumability
247249
* @param attemptCount Current reconnection attempt count for this specific stream
248250
*/
@@ -356,7 +358,7 @@ export class StreamableHTTPClientTransport implements Transport {
356358
throw new UnauthorizedError("No auth provider");
357359
}
358360

359-
const result = await auth(this._authProvider, { serverUrl: this._url, authorizationCode });
361+
const result = await auth(this._authProvider, { resourceServerUrl: this._url, authorizationCode, authServerUrl: this._authServerUrl });
360362
if (result !== "AUTHORIZED") {
361363
throw new UnauthorizedError("Failed to authorize");
362364
}
@@ -401,7 +403,17 @@ export class StreamableHTTPClientTransport implements Transport {
401403

402404
if (!response.ok) {
403405
if (response.status === 401 && this._authProvider) {
404-
const result = await auth(this._authProvider, { serverUrl: this._url });
406+
407+
const resourceMetadataUrl = extractResourceMetadataUrl(response);
408+
const protectedResourceMetadata = await discoverOAuthProtectedResourceMetadata(this._url, {
409+
resourceMetadataUrl: resourceMetadataUrl
410+
})
411+
if (protectedResourceMetadata.authorization_servers === undefined || protectedResourceMetadata.authorization_servers.length === 0) {
412+
throw new Error("Server does not speicify any authorization servers.");
413+
}
414+
this._authServerUrl = new URL(protectedResourceMetadata.authorization_servers[0]);
415+
416+
const result = await auth(this._authProvider, { resourceServerUrl: this._url, authServerUrl: this._authServerUrl });
405417
if (result !== "AUTHORIZED") {
406418
throw new UnauthorizedError();
407419
}
@@ -470,12 +482,12 @@ export class StreamableHTTPClientTransport implements Transport {
470482

471483
/**
472484
* Terminates the current session by sending a DELETE request to the server.
473-
*
485+
*
474486
* Clients that no longer need a particular session
475487
* (e.g., because the user is leaving the client application) SHOULD send an
476488
* HTTP DELETE to the MCP endpoint with the Mcp-Session-Id header to explicitly
477489
* terminate the session.
478-
*
490+
*
479491
* The server MAY respond with HTTP 405 Method Not Allowed, indicating that
480492
* the server does not allow clients to terminate sessions.
481493
*/

0 commit comments

Comments
 (0)