diff --git a/.changeset/pretty-plums-grab.md b/.changeset/pretty-plums-grab.md new file mode 100644 index 0000000000..d5f540fa73 --- /dev/null +++ b/.changeset/pretty-plums-grab.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/server': patch +--- + +Allow `buildOAuthProtectedResourceMetadata` callers to provide issuer-only authorization server metadata. diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index d2263d5829..dafd073da1 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -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, diff --git a/packages/server/src/server/middleware/oauthMetadata.ts b/packages/server/src/server/middleware/oauthMetadata.ts index fa5ac5f455..b8c8aea93d 100644 --- a/packages/server/src/server/middleware/oauthMetadata.ts +++ b/packages/server/src/server/middleware/oauthMetadata.ts @@ -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; /** * The public URL of this MCP server, used as the `resource` value in the @@ -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) { @@ -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, diff --git a/packages/server/test/server/oauthMetadata.test.ts b/packages/server/test/server/oauthMetadata.test.ts index fe3b3c4e20..fdf49ccf23 100644 --- a/packages/server/test/server/oauthMetadata.test.ts +++ b/packages/server/test/server/oauthMetadata.test.ts @@ -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, @@ -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/' } })