Feat: Separate authorization server and resource server on client auth flow (#416)

* add type and schema for protected resource metadata

* add private variable to keep track of
authorization server url to avoid fetching Protected Resource Metadata every time

* change auth flow to use authroization server and
add function to discover protected resource metadata

* add/modify tests for new added auth functions

* modify test to use the authorization server

* remove unused variable

* add resource parameter when building auth url

* Change resource to use the protected resource metadata

* resolve typo.

Co-authored-by: Paul Carleton <paulcarletonjr@gmail.com>

* Update src/client/sse.ts import.

Co-authored-by: Paul Carleton <paulcarletonjr@gmail.com>

* remove log.

Co-authored-by: Paul Carleton <paulcarletonjr@gmail.com>

* Update section title.

Co-authored-by: Paul Carleton <paulcarletonjr@gmail.com>

* remove log.

Co-authored-by: Paul Carleton <paulcarletonjr@gmail.com>

* remove resource parameter.

Co-authored-by: Paul Carleton <paulcarletonjr@gmail.com>

* remove resource parameter.

Co-authored-by: Paul Carleton <paulcarletonjr@gmail.com>

* remove resource parameter.

Co-authored-by: Paul Carleton <paulcarletonjr@gmail.com>

* remove resource parameter.

Co-authored-by: Paul Carleton <paulcarletonjr@gmail.com>

* remove resource parameter.

Co-authored-by: Paul Carleton <paulcarletonjr@gmail.com>

* remove resource parameter.

Co-authored-by: Paul Carleton <paulcarletonjr@gmail.com>

* remove resource parameter.

Co-authored-by: Paul Carleton <paulcarletonjr@gmail.com>

* remove resource parameter.

Co-authored-by: Paul Carleton <paulcarletonjr@gmail.com>

* remove resource parameter.

Co-authored-by: Paul Carleton <paulcarletonjr@gmail.com>

* make backwards compatibile

* s/resourceServerUrl/serverUrl/g for backwards compat

* fix test comment

---------

Co-authored-by: itsuki <itsuki.enjyoy@gmail.com>
Co-authored-by: Paul Carleton <paulcarletonjr@gmail.com>
Co-authored-by: Paul Carleton <paulc@anthropic.com>
This commit is contained in:
Itsuki 2025-05-21 22:55:38 +09:00 • committed by GitHub
commit 69dbfac0e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,6 +1,6 @@
import { Transport } from "../shared/transport.js";
import { isInitializedNotification, isJSONRPCRequest, isJSONRPCResponse, JSONRPCMessage, JSONRPCMessageSchema } from "../types.js";
import { auth, AuthResult, OAuthClientProvider, UnauthorizedError } from "./auth.js";
import { auth, AuthResult, extractResourceMetadataUrl, OAuthClientProvider, UnauthorizedError } from "./auth.js";
import { EventSourceParserStream } from "eventsource-parser/stream";
// Default reconnection options for StreamableHTTP connections
@ -119,6 +119,7 @@ export type StreamableHTTPClientTransportOptions = {
export class StreamableHTTPClientTransport implements Transport {
private _abortController?: AbortController;
private _url: URL;
private _resourceMetadataUrl?: URL;
private _requestInit?: RequestInit;
private _authProvider?: OAuthClientProvider;
private _sessionId?: string;
@ -133,6 +134,7 @@ export class StreamableHTTPClientTransport implements Transport {
opts?: StreamableHTTPClientTransportOptions,
) {
this._url = url;
this._resourceMetadataUrl = undefined;
this._requestInit = opts?.requestInit;
this._authProvider = opts?.authProvider;
this._sessionId = opts?.sessionId;
@ -146,7 +148,7 @@ export class StreamableHTTPClientTransport implements Transport {
let result: AuthResult;
try {
result = await auth(this._authProvider, { serverUrl: this._url });
result = await auth(this._authProvider, { serverUrl: this._url, resourceMetadataUrl: this._resourceMetadataUrl });
} catch (error) {
this.onerror?.(error as Error);
throw error;
@ -225,7 +227,7 @@ export class StreamableHTTPClientTransport implements Transport {
/**
* Calculates the next reconnection delay using backoff algorithm
*
*
* @param attempt Current reconnection attempt count for the specific stream
* @returns Time to wait in milliseconds before next reconnection attempt
*/
@ -242,7 +244,7 @@ export class StreamableHTTPClientTransport implements Transport {
/**
* Schedule a reconnection attempt with exponential backoff
*
*
* @param lastEventId The ID of the last received event for resumability
* @param attemptCount Current reconnection attempt count for this specific stream
*/
@ -356,7 +358,7 @@ export class StreamableHTTPClientTransport implements Transport {
throw new UnauthorizedError("No auth provider");
}
const result = await auth(this._authProvider, { serverUrl: this._url, authorizationCode });
const result = await auth(this._authProvider, { serverUrl: this._url, authorizationCode, resourceMetadataUrl: this._resourceMetadataUrl });
if (result !== "AUTHORIZED") {
throw new UnauthorizedError("Failed to authorize");
}
@ -401,7 +403,10 @@ export class StreamableHTTPClientTransport implements Transport {
if (!response.ok) {
if (response.status === 401 && this._authProvider) {
const result = await auth(this._authProvider, { serverUrl: this._url });
this._resourceMetadataUrl = extractResourceMetadataUrl(response);
const result = await auth(this._authProvider, { serverUrl: this._url, resourceMetadataUrl: this._resourceMetadataUrl });
if (result !== "AUTHORIZED") {
throw new UnauthorizedError();
}
@ -470,12 +475,12 @@ export class StreamableHTTPClientTransport implements Transport {
/**
* Terminates the current session by sending a DELETE request to the server.
*
*
* Clients that no longer need a particular session
* (e.g., because the user is leaving the client application) SHOULD send an
* HTTP DELETE to the MCP endpoint with the Mcp-Session-Id header to explicitly
* terminate the session.
*
*
* The server MAY respond with HTTP 405 Method Not Allowed, indicating that
* the server does not allow clients to terminate sessions.
*/