Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/pretty-plums-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/server': patch
---

Allow `buildOAuthProtectedResourceMetadata` callers to provide issuer-only authorization server metadata.
2 changes: 1 addition & 1 deletion packages/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export type { HostHeaderValidationResult } from './server/middleware/hostHeaderV
export { hostHeaderValidationResponse, localhostAllowedHostnames, validateHostHeader } from './server/middleware/hostHeaderValidation';
// OAuth discovery documents (RFC 9728 / RFC 8414) for web-standard hosts; the
// Express metadata router in @modelcontextprotocol/express adapts the same core.
export type { AuthMetadataOptions } from './server/middleware/oauthMetadata';
export type { AuthMetadataOptions, OAuthProtectedResourceMetadataOptions } from './server/middleware/oauthMetadata';
export {
buildOAuthProtectedResourceMetadata,
getOAuthProtectedResourceMetadataUrl,
Expand Down
30 changes: 20 additions & 10 deletions packages/server/src/server/middleware/oauthMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ import type { OAuthMetadata, OAuthProtectedResourceMetadata } from '@modelcontex
import { OAuthError, OAuthErrorCode } from '@modelcontextprotocol/core-internal';

/**
* Options for {@link oauthMetadataResponse} and
* {@link buildOAuthProtectedResourceMetadata}.
* Options for {@link buildOAuthProtectedResourceMetadata}. The RFC 9728
* Protected Resource Metadata document only needs to advertise the issuer of
* the Authorization Server, so callers that do not also serve AS metadata do
* not need to construct a full RFC 8414 metadata document.
*/
export interface AuthMetadataOptions {
export interface OAuthProtectedResourceMetadataOptions {
/**
* Authorization Server metadata (RFC 8414) for the AS this MCP server
* relies on. Served at `/.well-known/oauth-authorization-server` so
* legacy clients that probe the resource origin still discover the AS.
* Authorization Server issuer this MCP server relies on.
*/
oauthMetadata: OAuthMetadata;
oauthMetadata: Pick<OAuthMetadata, 'issuer'>;

/**
* The public URL of this MCP server, used as the `resource` value in the
Expand Down Expand Up @@ -44,6 +44,16 @@ export interface AuthMetadataOptions {
dangerouslyAllowInsecureIssuerUrl?: boolean;
}

/**
* Options for {@link oauthMetadataResponse}. This includes the full
* Authorization Server metadata (RFC 8414), because the response helper serves
* it at `/.well-known/oauth-authorization-server` so legacy clients that probe
* the resource origin still discover the AS.
*/
export interface AuthMetadataOptions extends OAuthProtectedResourceMetadataOptions {
oauthMetadata: OAuthMetadata;
}

function checkIssuerUrl(issuer: URL, allowInsecure: boolean | undefined): void {
// RFC 8414 technically does not permit a localhost HTTPS exemption, but it is necessary for local testing.
if (issuer.protocol !== 'https:' && issuer.hostname !== 'localhost' && issuer.hostname !== '127.0.0.1' && !allowInsecure) {
Expand All @@ -59,15 +69,15 @@ function checkIssuerUrl(issuer: URL, allowInsecure: boolean | undefined): void {

/**
* Derive the RFC 9728 Protected Resource Metadata document from
* {@link AuthMetadataOptions}, validating the Authorization Server issuer URL
* (HTTPS required outside localhost) in the process.
* {@link OAuthProtectedResourceMetadataOptions}, validating the Authorization
* Server issuer URL (HTTPS required outside localhost) in the process.
*
* `oauthMetadataResponse` and the Express `mcpAuthMetadataRouter` both build
* on this; use it directly when serving the document through your own
* routing — or call it once at startup to fail fast on a misconfigured
* issuer before any request arrives.
*/
export function buildOAuthProtectedResourceMetadata(options: AuthMetadataOptions): OAuthProtectedResourceMetadata {
export function buildOAuthProtectedResourceMetadata(options: OAuthProtectedResourceMetadataOptions): OAuthProtectedResourceMetadata {
checkIssuerUrl(new URL(options.oauthMetadata.issuer), options.dangerouslyAllowInsecureIssuerUrl);
return {
resource: options.resourceServerUrl.href,
Expand Down
14 changes: 13 additions & 1 deletion packages/server/test/server/oauthMetadata.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { OAuthMetadata } from '@modelcontextprotocol/core-internal';
import { describe, expect, it } from 'vitest';

import type { AuthMetadataOptions } from '../../src/server/middleware/oauthMetadata';
import type { AuthMetadataOptions, OAuthProtectedResourceMetadataOptions } from '../../src/server/middleware/oauthMetadata';
import {
buildOAuthProtectedResourceMetadata,
getOAuthProtectedResourceMetadataUrl,
Expand Down Expand Up @@ -35,6 +35,18 @@ describe('buildOAuthProtectedResourceMetadata', () => {
});
});

it('accepts issuer-only authorization server metadata', () => {
const prmOptions: OAuthProtectedResourceMetadataOptions = {
oauthMetadata: { issuer: 'https://auth.example.com/' },
resourceServerUrl: new URL('https://api.example.com/mcp')
};

expect(buildOAuthProtectedResourceMetadata(prmOptions)).toMatchObject({
resource: 'https://api.example.com/mcp',
authorization_servers: ['https://auth.example.com/']
});
});

it('rejects a non-HTTPS issuer', () => {
expect(() =>
buildOAuthProtectedResourceMetadata({ ...options, oauthMetadata: { ...oauthMetadata, issuer: 'http://auth.example.com/' } })
Expand Down
Loading