Skip to content

Commit 02df830

Browse files
authored
Add OAuth Protected Resource Metadata (PRM) handler (#75)
* prm metadata handler * comment fixing * export prm functions from index * add changeset * consistent JSON response formatting * use PRM schema from mcp ts library * include .js in import * Add PRM docs to readme
1 parent 80b0892 commit 02df830

File tree

4 files changed

+108
-14
lines changed

4 files changed

+108
-14
lines changed

.changeset/ripe-mails-doubt.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@vercel/mcp-adapter": minor
3+
---
4+
5+
Add RFC 9728 OAuth Protected Resource Metadata handler

README.md

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -223,25 +223,22 @@ When implementing authorization in MCP, you must define the OAuth [Protected Res
223223
Create a new file at `app/.well-known/oauth-protected-resource/route.ts`:
224224

225225
```typescript
226-
export async function GET(req: Request) {
227-
const origin = new URL(req.url).origin;
228-
229-
return Response.json({
230-
resource: `${origin}`,
231-
authorization_servers: [`https://authorization-server-issuer.com`],
232-
scopes_supported: ["openid"],
233-
resource_name: "MCP Server",
234-
resource_documentation: `${origin}/docs`
235-
});
236-
}
226+
import {
227+
protectedResourceHandler,
228+
metadataCorsOptionsRequestHandler,
229+
} from '@vercel/mcp-adapter'
230+
231+
const handler = protectedResourceHandler({
232+
// Specify the Issuer URL of the associated Authorization Server
233+
authServerUrls: ["https://auth-server.com"]
234+
})
235+
236+
export { handler as GET, metadataCorsOptionsRequestHandler as OPTIONS }
237237
```
238238

239239
This endpoint provides:
240240
- `resource`: The URL of your MCP server
241241
- `authorization_servers`: Array of OAuth authorization server Issuer URLs that can issue valid tokens
242-
- `scopes_supported`: Array of OAuth scopes supported by your server
243-
- `resource_name`: Human-readable name for your MCP server
244-
- `resource_documentation`: URL to your server's documentation
245242

246243
The path to this endpoint should match the `resourceMetadataPath` option in your `withMcpAuth` configuration,
247244
which by default is `/.well-known/oauth-protected-resource` (the full URL will be `https://your-domain.com/.well-known/oauth-protected-resource`).

src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,9 @@
22
export { default as createMcpHandler } from "./next";
33

44
export { withMcpAuth as experimental_withMcpAuth } from "./next/auth-wrapper";
5+
6+
export {
7+
protectedResourceHandler,
8+
generateProtectedResourceMetadata,
9+
metadataCorsOptionsRequestHandler,
10+
} from "./next/auth-metadata";

src/next/auth-metadata.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { OAuthProtectedResourceMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
2+
3+
/**
4+
* CORS headers for OAuth Protected Resource Metadata endpoint.
5+
* Configured to allow any origin to make the endpoint accessible to web-based MCP clients.
6+
*/
7+
const corsHeaders = {
8+
"Access-Control-Allow-Origin": "*",
9+
"Access-Control-Allow-Methods": "GET, OPTIONS",
10+
"Access-Control-Allow-Headers": "*",
11+
"Access-Control-Max-Age": "86400",
12+
};
13+
14+
/**
15+
* OAuth 2.0 Protected Resource Metadata endpoint based on RFC 9728.
16+
* @see https://datatracker.ietf.org/doc/html/rfc9728
17+
*
18+
* @param authServerUrls - Array of issuer URLs of the OAuth 2.0 Authorization Servers.
19+
* These should match the "issuer" field in the authorization servers'
20+
* OAuth metadata (RFC 8414).
21+
*/
22+
export function protectedResourceHandler({
23+
authServerUrls,
24+
}: {
25+
authServerUrls: string[];
26+
}) {
27+
return (req: Request) => {
28+
const origin = new URL(req.url).origin;
29+
30+
const metadata = generateProtectedResourceMetadata({
31+
authServerUrls,
32+
resourceUrl: origin,
33+
});
34+
35+
return new Response(JSON.stringify(metadata), {
36+
headers: {
37+
...corsHeaders,
38+
"Cache-Control": "max-age=3600",
39+
"Content-Type": "application/json",
40+
},
41+
});
42+
};
43+
}
44+
45+
/**
46+
* Generates protected resource metadata for the given auth server urls and
47+
* resource server url.
48+
*
49+
* @param authServerUrls - Array of issuer URLs of the authorization servers. Each URL should
50+
* match the "issuer" field in the respective authorization server's
51+
* OAuth metadata (RFC 8414).
52+
* @param resourceUrl - URL of the resource server
53+
* @param additionalMetadata - Additional metadata fields to include in the response
54+
* @returns Protected resource metadata, serializable to JSON
55+
*/
56+
export function generateProtectedResourceMetadata({
57+
authServerUrls,
58+
resourceUrl,
59+
additionalMetadata,
60+
}: {
61+
authServerUrls: string[];
62+
resourceUrl: string;
63+
additionalMetadata?: Partial<OAuthProtectedResourceMetadata>;
64+
}): OAuthProtectedResourceMetadata {
65+
return Object.assign(
66+
{
67+
resource: resourceUrl,
68+
authorization_servers: authServerUrls
69+
},
70+
additionalMetadata
71+
);
72+
}
73+
74+
/**
75+
* CORS options request handler for OAuth metadata endpoints.
76+
* Necessary for MCP clients that operate in web browsers.
77+
*/
78+
export function metadataCorsOptionsRequestHandler() {
79+
return () => {
80+
return new Response(null, {
81+
status: 200,
82+
headers: corsHeaders,
83+
});
84+
};
85+
}
86+

0 commit comments

Comments
 (0)