@@ -79,6 +79,156 @@ export type OAuthTokens = z.infer<typeof OAuthTokensSchema>;
7979export type OAuthClientMetadata = z . infer < typeof OAuthClientMetadataSchema > ;
8080export type OAuthClientInformation = z . infer < typeof OAuthClientInformationSchema > ;
8181
82+ /**
83+ * Implements an end-to-end OAuth client to be used with one MCP server.
84+ *
85+ * This client relies upon a concept of an authorized "session," the exact
86+ * meaning of which is application-defined. Tokens, authorization codes, and
87+ * code verifiers should not cross different sessions.
88+ */
89+ export interface OAuthClientProvider {
90+ /**
91+ * The URL to redirect the user agent to after authorization.
92+ *
93+ * If the client is not redirecting to localhost, `clientInformation` must be
94+ * implemented.
95+ */
96+ get redirectUrl ( ) : string | URL ;
97+
98+ /**
99+ * Metadata about this OAuth client.
100+ */
101+ get clientMetadata ( ) : OAuthClientMetadata ;
102+
103+ /**
104+ * Loads information about this OAuth client, as registered already with the
105+ * server, or returns `undefined` if the client is not registered with the
106+ * server.
107+ *
108+ * This method must be implemented _unless_ redirecting to `localhost`.
109+ */
110+ clientInformation ?( ) : OAuthClientInformation | undefined | Promise < OAuthClientInformation | undefined > ;
111+
112+ /**
113+ * If implemented, this permits the OAuth client to dynamically register with
114+ * the server. Client information saved this way should later be read via
115+ * `clientInformation()`.
116+ *
117+ * This method is not required to be implemented if redirecting to
118+ * `localhost`, or if client information is statically known (e.g.,
119+ * pre-registered).
120+ */
121+ saveClientInformation ?( clientInformation : OAuthClientInformation ) : void | Promise < void > ;
122+
123+ /**
124+ * Loads any existing OAuth tokens for the current session, or returns
125+ * `undefined` if there are no saved tokens.
126+ */
127+ tokens ( ) : OAuthTokens | undefined | Promise < OAuthTokens | undefined > ;
128+
129+ /**
130+ * Stores new OAuth tokens for the current session, after a successful
131+ * authorization.
132+ */
133+ saveTokens ( tokens : OAuthTokens ) : void | Promise < void > ;
134+
135+ /**
136+ * Invoked to redirect the user agent to the given URL to begin the authorization flow.
137+ */
138+ redirectToAuthorization ( authorizationUrl : URL ) : void | Promise < void > ;
139+
140+ /**
141+ * Saves a PKCE code verifier for the current session, before redirecting to
142+ * the authorization flow.
143+ */
144+ saveCodeVerifier ( codeVerifier : string ) : void | Promise < void > ;
145+
146+ /**
147+ * Loads the PKCE code verifier for the current session, necessary to validate
148+ * the authorization result.
149+ */
150+ codeVerifier ( ) : string | Promise < string > ;
151+ }
152+
153+ export type AuthResult = "AUTHORIZED" | "REDIRECT" ;
154+
155+ /**
156+ * Orchestrates the full auth flow with a server.
157+ *
158+ * This can be used as a single entry point for all authorization functionality,
159+ * instead of linking together the other lower-level functions in this module.
160+ */
161+ export async function auth (
162+ provider : OAuthClientProvider ,
163+ { serverUrl, authorizationCode } : { serverUrl : string | URL , authorizationCode ?: string } ) : Promise < AuthResult > {
164+ const metadata = await discoverOAuthMetadata ( serverUrl ) ;
165+
166+ // Handle client registration if needed
167+ const hostname = new URL ( provider . redirectUrl ) . hostname ;
168+ if ( hostname !== "localhost" && hostname !== "127.0.0.1" ) {
169+ if ( ! provider . clientInformation ) {
170+ throw new Error ( "OAuth client information is required when not redirecting to localhost" )
171+ }
172+
173+ let clientInformation = await Promise . resolve ( provider . clientInformation ( ) ) ;
174+ if ( ! clientInformation ) {
175+ if ( authorizationCode !== undefined ) {
176+ throw new Error ( "Existing OAuth client information is required when exchanging an authorization code" ) ;
177+ }
178+
179+ if ( ! provider . saveClientInformation ) {
180+ throw new Error ( "OAuth client information must be saveable when not provided and not redirecting to localhost" ) ;
181+ }
182+
183+ clientInformation = await registerClient ( serverUrl , {
184+ metadata,
185+ clientMetadata : provider . clientMetadata ,
186+ } ) ;
187+
188+ await provider . saveClientInformation ( clientInformation ) ;
189+ }
190+
191+ // TODO: Send clientInformation into auth flow
192+ }
193+
194+ // Exchange authorization code for tokens
195+ if ( authorizationCode !== undefined ) {
196+ const codeVerifier = await provider . codeVerifier ( ) ;
197+ const tokens = await exchangeAuthorization ( serverUrl , {
198+ metadata,
199+ authorizationCode,
200+ codeVerifier,
201+ } ) ;
202+
203+ await provider . saveTokens ( tokens ) ;
204+ return "AUTHORIZED" ;
205+ }
206+
207+ const tokens = await provider . tokens ( ) ;
208+
209+ // Handle token refresh or new authorization
210+ if ( tokens ?. refresh_token ) {
211+ try {
212+ // Attempt to refresh the token
213+ const newTokens = await refreshAuthorization ( serverUrl , {
214+ metadata,
215+ refreshToken : tokens . refresh_token ,
216+ } ) ;
217+
218+ await provider . saveTokens ( newTokens ) ;
219+ return "AUTHORIZED" ;
220+ } catch ( error ) {
221+ console . error ( "Could not refresh OAuth tokens:" , error ) ;
222+ }
223+ }
224+
225+ // Start new authorization flow
226+ const { authorizationUrl, codeVerifier } = await startAuthorization ( serverUrl , { metadata, redirectUrl : provider . redirectUrl } ) ;
227+ await provider . saveCodeVerifier ( codeVerifier ) ;
228+ await provider . redirectToAuthorization ( authorizationUrl ) ;
229+ return "REDIRECT" ;
230+ }
231+
82232/**
83233 * Looks up RFC 8414 OAuth 2.0 Authorization Server Metadata.
84234 *
0 commit comments