Skip to content

Commit 722c55c

Browse files
committed
Implement SEP-990 Enterprise Managed OAuth
1 parent f82c997 commit 722c55c

File tree

8 files changed

+1739
-1
lines changed

8 files changed

+1739
-1
lines changed

docs/client.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,59 @@ These examples show how to:
5858
- Perform dynamic client registration if needed.
5959
- Acquire access tokens.
6060
- Attach OAuth credentials to Streamable HTTP requests.
61+
62+
#### Cross-App Access Middleware
63+
64+
The `withCrossAppAccess` middleware enables secure authentication for MCP clients accessing protected servers through OAuth-based Cross-App Access flows. It automatically handles token acquisition and adds Authorization headers to requests.
65+
66+
```typescript
67+
import { Client } from '@modelcontextprotocol/client';
68+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
69+
import { withCrossAppAccess } from '@modelcontextprotocol/client';
70+
71+
// Configure Cross-App Access middleware
72+
const enhancedFetch = withCrossAppAccess({
73+
idpUrl: 'https://idp.example.com',
74+
mcpResourceUrl: 'https://mcp-server.example.com',
75+
mcpAuthorisationServerUrl: 'https://mcp-auth.example.com',
76+
idToken: 'your-id-token',
77+
idpClientId: 'your-idp-client-id',
78+
idpClientSecret: 'your-idp-client-secret',
79+
mcpClientId: 'your-mcp-client-id',
80+
mcpClientSecret: 'your-mcp-client-secret',
81+
scope: ['read', 'write'] // Optional scopes
82+
})(fetch);
83+
84+
// Use the enhanced fetch with your client transport
85+
const transport = new StreamableHTTPClientTransport(
86+
new URL('https://mcp-server.example.com/mcp'),
87+
enhancedFetch
88+
);
89+
90+
const client = new Client({
91+
name: 'secure-client',
92+
version: '1.0.0'
93+
});
94+
95+
await client.connect(transport);
96+
```
97+
98+
The middleware performs a two-step OAuth flow:
99+
100+
1. Exchanges your ID token for an authorization grant from the IdP
101+
2. Exchanges the grant for an access token from the MCP authorization server
102+
3. Automatically adds the access token to all subsequent requests
103+
104+
**Configuration Options:**
105+
106+
- **`idpUrl`**: Identity Provider's base URL for OAuth discovery
107+
- **`idToken`**: Identity token obtained from user authentication with the IdP
108+
- **`idpClientId`** / **`idpClientSecret`**: Credentials for authentication with the IdP
109+
- **`mcpResourceUrl`**: MCP resource server URL (used in token exchange request)
110+
- **`mcpAuthorisationServerUrl`**: MCP authorization server URL for OAuth discovery
111+
- **`mcpClientId`** / **`mcpClientSecret`**: Credentials for authentication with the MCP server
112+
- **`scope`**: Optional array of scope strings (e.g., `['read', 'write']`)
113+
114+
**Token Caching:**
115+
116+
The middleware caches the access token after the first successful exchange, so the token exchange flow only happens once. Subsequent requests reuse the cached token without additional OAuth calls.

packages/client/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"eventsource-parser": "catalog:runtimeClientOnly",
5050
"jose": "catalog:runtimeClientOnly",
5151
"pkce-challenge": "catalog:runtimeShared",
52+
"qs": "catalog:runtimeClientOnly",
5253
"zod": "catalog:runtimeShared"
5354
},
5455
"peerDependencies": {
@@ -73,6 +74,7 @@
7374
"@types/content-type": "catalog:devTools",
7475
"@types/cross-spawn": "catalog:devTools",
7576
"@types/eventsource": "catalog:devTools",
77+
"@types/qs": "^6.9.18",
7678
"@typescript/native-preview": "catalog:devTools",
7779
"@eslint/js": "catalog:devTools",
7880
"eslint": "catalog:devTools",

packages/client/src/client/middleware.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { FetchLike } from '@modelcontextprotocol/core';
22

33
import type { OAuthClientProvider } from './auth.js';
44
import { auth, extractWWWAuthenticateParams, UnauthorizedError } from './auth.js';
5+
import { getAccessToken, XAAOptions } from './xaa-util.js';
56

67
/**
78
* Middleware function that wraps and enhances fetch functionality.
@@ -234,6 +235,35 @@ export const withLogging = (options: LoggingOptions = {}): Middleware => {
234235
};
235236
};
236237

238+
/**
239+
* Creates a fetch wrapper that handles Cross App Access authentication automatically.
240+
*
241+
* This wrapper will:
242+
* - Add Authorization headers with access tokens
243+
*
244+
* @param options - XAA configuration options
245+
* @returns A fetch middleware function
246+
*/
247+
export const withCrossAppAccess = (options: XAAOptions): Middleware => {
248+
return wrappedFetchFunction => {
249+
let accessToken: string | undefined = undefined;
250+
251+
return async (url, init = {}): Promise<Response> => {
252+
if (!accessToken) {
253+
accessToken = await getAccessToken(options, wrappedFetchFunction);
254+
}
255+
256+
const headers = new Headers(init.headers);
257+
258+
headers.set('Authorization', `Bearer ${accessToken}`);
259+
260+
init.headers = headers;
261+
262+
return wrappedFetchFunction(url, init);
263+
};
264+
};
265+
};
266+
237267
/**
238268
* Composes multiple fetch middleware functions into a single middleware pipeline.
239269
* Middleware are applied in the order they appear, creating a chain of handlers.

0 commit comments

Comments
 (0)