Skip to content

Commit 26a4929

Browse files
authored
Merge branch 'main' into feat/stdio-protocol-version-1468
2 parents 1c1cc4e + ccb78f2 commit 26a4929

File tree

16 files changed

+1750
-37
lines changed

16 files changed

+1750
-37
lines changed

docs/client.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import {
1919
Client,
2020
ClientCredentialsProvider,
2121
createMiddleware,
22+
CrossAppAccessProvider,
23+
discoverAndRequestJwtAuthGrant,
2224
PrivateKeyJwtProvider,
2325
ProtocolError,
2426
SdkError,
@@ -152,6 +154,51 @@ For user-facing applications, implement the {@linkcode @modelcontextprotocol/cli
152154

153155
For a complete working OAuth flow, see [`simpleOAuthClient.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleOAuthClient.ts) and [`simpleOAuthClientProvider.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleOAuthClientProvider.ts).
154156

157+
### Cross-App Access (Enterprise Managed Authorization)
158+
159+
{@linkcode @modelcontextprotocol/client!client/authExtensions.CrossAppAccessProvider | CrossAppAccessProvider} implements Enterprise Managed Authorization (SEP-990) for scenarios where users authenticate with an enterprise identity provider (IdP) and clients need to access protected MCP servers on their behalf.
160+
161+
This provider handles a two-step OAuth flow:
162+
1. Exchange the user's ID Token from the enterprise IdP for a JWT Authorization Grant (JAG) via RFC 8693 token exchange
163+
2. Exchange the JAG for an access token from the MCP server via RFC 7523 JWT bearer grant
164+
165+
```ts source="../examples/client/src/clientGuide.examples.ts#auth_crossAppAccess"
166+
const authProvider = new CrossAppAccessProvider({
167+
assertion: async ctx => {
168+
// ctx provides: authorizationServerUrl, resourceUrl, scope, fetchFn
169+
const result = await discoverAndRequestJwtAuthGrant({
170+
idpUrl: 'https://idp.example.com',
171+
audience: ctx.authorizationServerUrl,
172+
resource: ctx.resourceUrl,
173+
idToken: await getIdToken(),
174+
clientId: 'my-idp-client',
175+
clientSecret: 'my-idp-secret',
176+
scope: ctx.scope,
177+
fetchFn: ctx.fetchFn
178+
});
179+
return result.jwtAuthGrant;
180+
},
181+
clientId: 'my-mcp-client',
182+
clientSecret: 'my-mcp-secret'
183+
});
184+
185+
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'), { authProvider });
186+
```
187+
188+
The `assertion` callback receives a context object with:
189+
- `authorizationServerUrl` – The MCP server's authorization server (discovered automatically)
190+
- `resourceUrl` – The MCP resource URL (discovered automatically)
191+
- `scope` – Optional scope passed to `auth()` or from `clientMetadata`
192+
- `fetchFn` – Fetch implementation to use for HTTP requests
193+
194+
For manual control over the token exchange steps, use the Layer 2 utilities from `@modelcontextprotocol/client`:
195+
- `requestJwtAuthorizationGrant()` – Exchange ID Token for JAG at IdP
196+
- `discoverAndRequestJwtAuthGrant()` – Discovery + JAG acquisition
197+
- `exchangeJwtAuthGrant()` – Exchange JAG for access token at MCP server
198+
199+
> [!NOTE]
200+
> See [RFC 8693 (Token Exchange)](https://datatracker.ietf.org/doc/html/rfc8693), [RFC 7523 (JWT Bearer Grant)](https://datatracker.ietf.org/doc/html/rfc7523), and [RFC 9728 (Resource Discovery)](https://datatracker.ietf.org/doc/html/rfc9728) for the underlying OAuth standards.
201+
155202
## Tools
156203

157204
Tools are callable actions offered by servers — discovering and invoking them is usually how your client enables an LLM to take action (see [Tools](https://modelcontextprotocol.io/docs/learn/server-concepts#tools) in the MCP overview).

examples/client/src/clientGuide.examples.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
Client,
1515
ClientCredentialsProvider,
1616
createMiddleware,
17+
CrossAppAccessProvider,
18+
discoverAndRequestJwtAuthGrant,
1719
PrivateKeyJwtProvider,
1820
ProtocolError,
1921
SdkError,
@@ -135,6 +137,33 @@ async function auth_privateKeyJwt(pemEncodedKey: string) {
135137
return transport;
136138
}
137139

140+
/** Example: Cross-App Access (SEP-990 Enterprise Managed Authorization). */
141+
async function auth_crossAppAccess(getIdToken: () => Promise<string>) {
142+
//#region auth_crossAppAccess
143+
const authProvider = new CrossAppAccessProvider({
144+
assertion: async ctx => {
145+
// ctx provides: authorizationServerUrl, resourceUrl, scope, fetchFn
146+
const result = await discoverAndRequestJwtAuthGrant({
147+
idpUrl: 'https://idp.example.com',
148+
audience: ctx.authorizationServerUrl,
149+
resource: ctx.resourceUrl,
150+
idToken: await getIdToken(),
151+
clientId: 'my-idp-client',
152+
clientSecret: 'my-idp-secret',
153+
scope: ctx.scope,
154+
fetchFn: ctx.fetchFn
155+
});
156+
return result.jwtAuthGrant;
157+
},
158+
clientId: 'my-mcp-client',
159+
clientSecret: 'my-mcp-secret'
160+
});
161+
162+
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'), { authProvider });
163+
//#endregion auth_crossAppAccess
164+
return transport;
165+
}
166+
138167
// ---------------------------------------------------------------------------
139168
// Using server features
140169
// ---------------------------------------------------------------------------
@@ -513,6 +542,7 @@ void disconnect_streamableHttp;
513542
void serverInstructions_basic;
514543
void auth_clientCredentials;
515544
void auth_privateKeyJwt;
545+
void auth_crossAppAccess;
516546
void callTool_basic;
517547
void callTool_structuredOutput;
518548
void callTool_progress;

packages/client/src/client/auth.ts

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,50 @@ export interface OAuthClientProvider {
184184
*/
185185
prepareTokenRequest?(scope?: string): URLSearchParams | Promise<URLSearchParams | undefined> | undefined;
186186

187+
/**
188+
* Saves the authorization server URL after RFC 9728 discovery.
189+
* This method is called by {@linkcode auth} after successful discovery of the
190+
* authorization server via protected resource metadata.
191+
*
192+
* Providers implementing Cross-App Access or other flows that need access to
193+
* the discovered authorization server URL should implement this method.
194+
*
195+
* @param authorizationServerUrl - The authorization server URL discovered via RFC 9728
196+
*/
197+
saveAuthorizationServerUrl?(authorizationServerUrl: string): void | Promise<void>;
198+
199+
/**
200+
* Returns the previously saved authorization server URL, if available.
201+
*
202+
* Providers implementing Cross-App Access can use this to access the
203+
* authorization server URL discovered during the OAuth flow.
204+
*
205+
* @returns The authorization server URL, or `undefined` if not available
206+
*/
207+
authorizationServerUrl?(): string | undefined | Promise<string | undefined>;
208+
209+
/**
210+
* Saves the resource URL after RFC 9728 discovery.
211+
* This method is called by {@linkcode auth} after successful discovery of the
212+
* resource metadata.
213+
*
214+
* Providers implementing Cross-App Access or other flows that need access to
215+
* the discovered resource URL should implement this method.
216+
*
217+
* @param resourceUrl - The resource URL discovered via RFC 9728
218+
*/
219+
saveResourceUrl?(resourceUrl: string): void | Promise<void>;
220+
221+
/**
222+
* Returns the previously saved resource URL, if available.
223+
*
224+
* Providers implementing Cross-App Access can use this to access the
225+
* resource URL discovered during the OAuth flow.
226+
*
227+
* @returns The resource URL, or `undefined` if not available
228+
*/
229+
resourceUrl?(): string | undefined | Promise<string | undefined>;
230+
187231
/**
188232
* Saves the OAuth discovery state after RFC 9728 and authorization server metadata
189233
* discovery. Providers can persist this state to avoid redundant discovery requests
@@ -307,7 +351,7 @@ export function selectClientAuthMethod(clientInformation: OAuthClientInformation
307351
* @param params - URL search parameters to modify
308352
* @throws {Error} When required credentials are missing
309353
*/
310-
function applyClientAuthentication(
354+
export function applyClientAuthentication(
311355
method: ClientAuthMethod,
312356
clientInformation: OAuthClientInformation,
313357
headers: Headers,
@@ -501,8 +545,16 @@ async function authInternal(
501545
});
502546
}
503547

548+
// Save authorization server URL for providers that need it (e.g., CrossAppAccessProvider)
549+
await provider.saveAuthorizationServerUrl?.(String(authorizationServerUrl));
550+
504551
const resource: URL | undefined = await selectResourceURL(serverUrl, provider, resourceMetadata);
505552

553+
// Save resource URL for providers that need it (e.g., CrossAppAccessProvider)
554+
if (resource) {
555+
await provider.saveResourceUrl?.(String(resource));
556+
}
557+
506558
// Apply scope selection strategy (SEP-835):
507559
// 1. WWW-Authenticate scope (passed via `scope` param)
508560
// 2. PRM scopes_supported
@@ -562,6 +614,7 @@ async function authInternal(
562614
metadata,
563615
resource,
564616
authorizationCode,
617+
scope,
565618
fetchFn
566619
});
567620

@@ -1401,21 +1454,25 @@ export async function fetchToken(
14011454
metadata,
14021455
resource,
14031456
authorizationCode,
1457+
scope,
14041458
fetchFn
14051459
}: {
14061460
metadata?: AuthorizationServerMetadata;
14071461
resource?: URL;
14081462
/** Authorization code for the default `authorization_code` grant flow */
14091463
authorizationCode?: string;
1464+
/** Optional scope parameter from auth() options */
1465+
scope?: string;
14101466
fetchFn?: FetchLike;
14111467
} = {}
14121468
): Promise<OAuthTokens> {
1413-
const scope = provider.clientMetadata.scope;
1469+
// Prefer scope from options, fallback to provider.clientMetadata.scope
1470+
const effectiveScope = scope ?? provider.clientMetadata.scope;
14141471

14151472
// Use provider's prepareTokenRequest if available, otherwise fall back to authorization_code
14161473
let tokenRequestParams: URLSearchParams | undefined;
14171474
if (provider.prepareTokenRequest) {
1418-
tokenRequestParams = await provider.prepareTokenRequest(scope);
1475+
tokenRequestParams = await provider.prepareTokenRequest(effectiveScope);
14191476
}
14201477

14211478
// Default to authorization_code grant if no custom prepareTokenRequest

0 commit comments

Comments
 (0)