From 67c1dfe09fe810efb507c60a6fd535bd1984f8ff Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 20 Nov 2025 13:25:53 +0000 Subject: [PATCH 1/6] [auth] Add metadata suite for running metadata discovery tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new 'metadata' suite that runs just the auth/metadata-* scenarios for faster iteration when testing metadata discovery specifically. Usage: node dist/index.mjs client --suite metadata --command "..." 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/index.ts | 8 +++++--- src/scenarios/client/auth/discovery-metadata.ts | 5 +++++ src/scenarios/index.ts | 3 +++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 1cb6b38..21145d5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,8 @@ import { listScenarios, listClientScenarios, listActiveClientScenarios, - listAuthScenarios + listAuthScenarios, + listMetadataScenarios } from './scenarios'; import { ConformanceCheck } from './types'; import { ClientOptionsSchema, ServerOptionsSchema } from './schemas'; @@ -51,7 +52,8 @@ program } const suites: Record string[]> = { - auth: listAuthScenarios + auth: listAuthScenarios, + metadata: listMetadataScenarios }; const suiteName = options.suite.toLowerCase(); @@ -147,7 +149,7 @@ program console.error('Either --scenario or --suite is required'); console.error('\nAvailable client scenarios:'); listScenarios().forEach((s) => console.error(` - ${s}`)); - console.error('\nAvailable suites: auth'); + console.error('\nAvailable suites: auth, metadata'); process.exit(1); } diff --git a/src/scenarios/client/auth/discovery-metadata.ts b/src/scenarios/client/auth/discovery-metadata.ts index 56a9694..86f1d00 100644 --- a/src/scenarios/client/auth/discovery-metadata.ts +++ b/src/scenarios/client/auth/discovery-metadata.ts @@ -217,3 +217,8 @@ export const AuthMetadataVar3Scenario = createMetadataScenario( // Export all scenarios as an array for convenience export const metadataScenarios = SCENARIO_CONFIGS.map(createMetadataScenario); + +// Export function to list metadata scenario names (for suite support) +export function listMetadataScenarios(): string[] { + return metadataScenarios.map((s) => s.name); +} diff --git a/src/scenarios/index.ts b/src/scenarios/index.ts index 16e01ce..3180c26 100644 --- a/src/scenarios/index.ts +++ b/src/scenarios/index.ts @@ -46,6 +46,7 @@ import { } from './server/prompts.js'; import { authScenariosList } from './client/auth/index.js'; +import { listMetadataScenarios } from './client/auth/discovery-metadata.js'; // Pending client scenarios (not yet fully tested/implemented) const pendingClientScenariosList: ClientScenario[] = [ @@ -151,3 +152,5 @@ export function listActiveClientScenarios(): string[] { export function listAuthScenarios(): string[] { return authScenariosList.map((scenario) => scenario.name); } + +export { listMetadataScenarios }; From f559fe59ee847eb82865d0f3c8fd1ec98b65bab3 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 20 Nov 2025 13:26:12 +0000 Subject: [PATCH 2/6] Don't require lefthook to be installed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Set assert_lefthook_installed to false so the hooks gracefully skip if lefthook is not installed on the system. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lefthook.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lefthook.yml b/lefthook.yml index 3399e5f..445ba02 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -1,7 +1,7 @@ # lefthook.yml # Configuration reference: https://lefthook.dev/configuration/ -assert_lefthook_installed: true +assert_lefthook_installed: false output: - meta # Print lefthook version From 8a59560188f1a7328b8f86397c54a5738f5b4309 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 20 Nov 2025 20:27:14 +0000 Subject: [PATCH 3/6] Add token endpoint auth method conformance tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add three new scenarios to test that clients correctly use the appropriate authentication method based on server metadata: - auth/token-endpoint-auth-basic: Tests client_secret_basic (HTTP Basic) - auth/token-endpoint-auth-post: Tests client_secret_post - auth/token-endpoint-auth-none: Tests public client (no auth) Each scenario configures the server to only support one auth method and verifies the client uses the correct method in token requests. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/scenarios/client/auth/index.ts | 10 +- .../client/auth/token-endpoint-auth.ts | 336 ++++++++++++++++++ 2 files changed, 345 insertions(+), 1 deletion(-) create mode 100644 src/scenarios/client/auth/token-endpoint-auth.ts diff --git a/src/scenarios/client/auth/index.ts b/src/scenarios/client/auth/index.ts index f78d2e1..140c94c 100644 --- a/src/scenarios/client/auth/index.ts +++ b/src/scenarios/client/auth/index.ts @@ -10,6 +10,11 @@ import { ScopeOmittedWhenUndefinedScenario, ScopeStepUpAuthScenario } from './scope-handling.js'; +import { + ClientSecretBasicAuthScenario, + ClientSecretPostAuthScenario, + PublicClientAuthScenario +} from './token-endpoint-auth.js'; export const authScenariosList: Scenario[] = [ ...metadataScenarios, @@ -18,5 +23,8 @@ export const authScenariosList: Scenario[] = [ new ScopeFromWwwAuthenticateScenario(), new ScopeFromScopesSupportedScenario(), new ScopeOmittedWhenUndefinedScenario(), - new ScopeStepUpAuthScenario() + new ScopeStepUpAuthScenario(), + new ClientSecretBasicAuthScenario(), + new ClientSecretPostAuthScenario(), + new PublicClientAuthScenario() ]; diff --git a/src/scenarios/client/auth/token-endpoint-auth.ts b/src/scenarios/client/auth/token-endpoint-auth.ts new file mode 100644 index 0000000..872a09e --- /dev/null +++ b/src/scenarios/client/auth/token-endpoint-auth.ts @@ -0,0 +1,336 @@ +import type { Scenario, ConformanceCheck } from '../../../types.js'; +import { ScenarioUrls } from '../../../types.js'; +import { createServer } from './helpers/createServer.js'; +import { ServerLifecycle } from './helpers/serverLifecycle.js'; +import { SpecReferences } from './spec-references.js'; +import { MockTokenVerifier } from './helpers/mockTokenVerifier.js'; +import { createRequestLogger } from '../../request-logger.js'; +import express, { Request, Response } from 'express'; + +type AuthMethod = 'client_secret_basic' | 'client_secret_post' | 'none'; + +interface AuthServerOptions { + tokenVerifier?: MockTokenVerifier; + tokenEndpointAuthMethodsSupported: string[]; + expectedAuthMethod: AuthMethod; + onTokenRequest?: (requestData: { + authorizationHeader?: string; + bodyClientSecret?: string; + timestamp: string; + }) => void; +} + +function detectAuthMethod( + authorizationHeader?: string, + bodyClientSecret?: string +): AuthMethod { + if (authorizationHeader?.startsWith('Basic ')) { + return 'client_secret_basic'; + } + if (bodyClientSecret) { + return 'client_secret_post'; + } + return 'none'; +} + +function validateBasicAuthFormat(authorizationHeader: string): { + valid: boolean; + error?: string; +} { + const encoded = authorizationHeader.substring('Basic '.length); + try { + const decoded = Buffer.from(encoded, 'base64').toString('utf-8'); + if (!decoded.includes(':')) { + return { valid: false, error: 'missing colon separator' }; + } + return { valid: true }; + } catch { + return { valid: false, error: 'base64 decoding failed' }; + } +} + +function createAuthServerForTokenAuth( + checks: ConformanceCheck[], + getAuthBaseUrl: () => string, + options: AuthServerOptions +): express.Application { + const { + tokenVerifier, + tokenEndpointAuthMethodsSupported, + expectedAuthMethod, + onTokenRequest + } = options; + + const authRoutes = { + authorization_endpoint: '/authorize', + token_endpoint: '/token', + registration_endpoint: '/register' + }; + + const app = express(); + app.use(express.json()); + app.use(express.urlencoded({ extended: true })); + + app.use( + createRequestLogger(checks, { + incomingId: 'incoming-auth-request', + outgoingId: 'outgoing-auth-response' + }) + ); + + app.get( + '/.well-known/oauth-authorization-server', + (req: Request, res: Response) => { + checks.push({ + id: 'authorization-server-metadata', + name: 'AuthorizationServerMetadata', + description: 'Client requested authorization server metadata', + status: 'SUCCESS', + timestamp: new Date().toISOString(), + specReferences: [ + SpecReferences.RFC_AUTH_SERVER_METADATA_REQUEST, + SpecReferences.MCP_AUTH_DISCOVERY + ], + details: { + url: req.url, + path: req.path + } + }); + + res.json({ + issuer: getAuthBaseUrl(), + authorization_endpoint: `${getAuthBaseUrl()}${authRoutes.authorization_endpoint}`, + token_endpoint: `${getAuthBaseUrl()}${authRoutes.token_endpoint}`, + registration_endpoint: `${getAuthBaseUrl()}${authRoutes.registration_endpoint}`, + response_types_supported: ['code'], + grant_types_supported: ['authorization_code', 'refresh_token'], + code_challenge_methods_supported: ['S256'], + token_endpoint_auth_methods_supported: tokenEndpointAuthMethodsSupported + }); + } + ); + + app.get(authRoutes.authorization_endpoint, (req: Request, res: Response) => { + checks.push({ + id: 'authorization-request', + name: 'AuthorizationRequest', + description: 'Client made authorization request', + status: 'SUCCESS', + timestamp: new Date().toISOString(), + specReferences: [SpecReferences.OAUTH_2_1_AUTHORIZATION_ENDPOINT], + details: { query: req.query } + }); + + const redirectUri = req.query.redirect_uri as string; + const state = req.query.state as string; + const redirectUrl = new URL(redirectUri); + redirectUrl.searchParams.set('code', 'test-auth-code'); + if (state) { + redirectUrl.searchParams.set('state', state); + } + + res.redirect(redirectUrl.toString()); + }); + + app.post(authRoutes.token_endpoint, (req: Request, res: Response) => { + const timestamp = new Date().toISOString(); + const authorizationHeader = req.headers.authorization as string | undefined; + const bodyClientSecret = req.body.client_secret; + + checks.push({ + id: 'token-request', + name: 'TokenRequest', + description: 'Client requested access token', + status: 'SUCCESS', + timestamp, + specReferences: [SpecReferences.OAUTH_2_1_TOKEN], + details: { + endpoint: '/token', + grantType: req.body.grant_type, + hasAuthorizationHeader: !!authorizationHeader, + hasBodyClientSecret: !!bodyClientSecret + } + }); + + if (onTokenRequest) { + onTokenRequest({ authorizationHeader, bodyClientSecret, timestamp }); + } + + const token = `test-token-${Date.now()}`; + if (tokenVerifier) { + tokenVerifier.registerToken(token, []); + } + + res.json({ + access_token: token, + token_type: 'Bearer', + expires_in: 3600 + }); + }); + + app.post(authRoutes.registration_endpoint, (req: Request, res: Response) => { + const clientId = `test-client-${Date.now()}`; + const clientSecret = + expectedAuthMethod === 'none' ? undefined : `test-secret-${Date.now()}`; + + checks.push({ + id: 'client-registration', + name: 'ClientRegistration', + description: 'Client registered with authorization server', + status: 'SUCCESS', + timestamp: new Date().toISOString(), + specReferences: [SpecReferences.MCP_DCR], + details: { + endpoint: '/register', + clientName: req.body.client_name, + tokenEndpointAuthMethod: expectedAuthMethod + } + }); + + res.status(201).json({ + client_id: clientId, + ...(clientSecret && { client_secret: clientSecret }), + client_name: req.body.client_name || 'test-client', + redirect_uris: req.body.redirect_uris || [], + token_endpoint_auth_method: expectedAuthMethod + }); + }); + + return app; +} + +const AUTH_METHOD_NAMES: Record = { + client_secret_basic: 'HTTP Basic authentication (client_secret_basic)', + client_secret_post: 'client_secret_post', + none: 'no authentication (public client)' +}; + +class TokenEndpointAuthScenario implements Scenario { + name: string; + description: string; + private expectedAuthMethod: AuthMethod; + private authServer = new ServerLifecycle(); + private server = new ServerLifecycle(); + private checks: ConformanceCheck[] = []; + + constructor(expectedAuthMethod: AuthMethod) { + this.expectedAuthMethod = expectedAuthMethod; + this.name = `auth/token-endpoint-auth-${expectedAuthMethod === 'client_secret_basic' ? 'basic' : expectedAuthMethod === 'client_secret_post' ? 'post' : 'none'}`; + this.description = `Tests that client uses ${AUTH_METHOD_NAMES[expectedAuthMethod]} when server only supports ${expectedAuthMethod}`; + } + + async start(): Promise { + this.checks = []; + const tokenVerifier = new MockTokenVerifier(this.checks, []); + + const authApp = createAuthServerForTokenAuth( + this.checks, + this.authServer.getUrl, + { + tokenVerifier, + tokenEndpointAuthMethodsSupported: [this.expectedAuthMethod], + expectedAuthMethod: this.expectedAuthMethod, + onTokenRequest: (data) => { + const actualMethod = detectAuthMethod( + data.authorizationHeader, + data.bodyClientSecret + ); + const isCorrect = actualMethod === this.expectedAuthMethod; + + // For basic auth, also validate the format + let formatError: string | undefined; + if ( + actualMethod === 'client_secret_basic' && + data.authorizationHeader + ) { + const validation = validateBasicAuthFormat( + data.authorizationHeader + ); + if (!validation.valid) { + formatError = validation.error; + } + } + + const status = isCorrect && !formatError ? 'SUCCESS' : 'FAILURE'; + let description: string; + + if (formatError) { + description = `Client sent Basic auth header but ${formatError}`; + } else if (isCorrect) { + description = `Client correctly used ${AUTH_METHOD_NAMES[this.expectedAuthMethod]} for token endpoint`; + } else { + description = `Client used ${actualMethod} but server only supports ${this.expectedAuthMethod}`; + } + + this.checks.push({ + id: 'token-endpoint-auth-method', + name: 'Token endpoint authentication method', + description, + status, + timestamp: data.timestamp, + specReferences: [SpecReferences.OAUTH_2_1_TOKEN], + details: { + expectedAuthMethod: this.expectedAuthMethod, + actualAuthMethod: actualMethod, + hasAuthorizationHeader: !!data.authorizationHeader, + hasBodyClientSecret: !!data.bodyClientSecret, + ...(formatError && { formatError }) + } + }); + } + } + ); + await this.authServer.start(authApp); + + const app = createServer( + this.checks, + this.server.getUrl, + this.authServer.getUrl, + { + prmPath: '/.well-known/oauth-protected-resource/mcp', + requiredScopes: [], + tokenVerifier + } + ); + await this.server.start(app); + + return { serverUrl: `${this.server.getUrl()}/mcp` }; + } + + async stop() { + await this.authServer.stop(); + await this.server.stop(); + } + + getChecks(): ConformanceCheck[] { + if (!this.checks.some((c) => c.id === 'token-endpoint-auth-method')) { + this.checks.push({ + id: 'token-endpoint-auth-method', + name: 'Token endpoint authentication method', + description: 'Client did not make a token request', + status: 'FAILURE', + timestamp: new Date().toISOString(), + specReferences: [SpecReferences.OAUTH_2_1_TOKEN] + }); + } + return this.checks; + } +} + +export class ClientSecretBasicAuthScenario extends TokenEndpointAuthScenario { + constructor() { + super('client_secret_basic'); + } +} + +export class ClientSecretPostAuthScenario extends TokenEndpointAuthScenario { + constructor() { + super('client_secret_post'); + } +} + +export class PublicClientAuthScenario extends TokenEndpointAuthScenario { + constructor() { + super('none'); + } +} From 0d0c631de59cd7619772ac75f97bd571edfd640c Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 20 Nov 2025 20:31:31 +0000 Subject: [PATCH 4/6] Refactor token endpoint auth tests to use shared createAuthServer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend createAuthServer helper with: - tokenEndpointAuthMethodsSupported option for metadata - onTokenRequest callback now receives full Request object - onRegistrationRequest callback for custom client credentials This eliminates the duplicate auth server implementation in token-endpoint-auth.ts and reduces code by ~140 lines. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../client/auth/helpers/createAuthServer.ts | 57 ++-- .../client/auth/token-endpoint-auth.ts | 272 ++++-------------- 2 files changed, 94 insertions(+), 235 deletions(-) diff --git a/src/scenarios/client/auth/helpers/createAuthServer.ts b/src/scenarios/client/auth/helpers/createAuthServer.ts index 7828990..b7b40b1 100644 --- a/src/scenarios/client/auth/helpers/createAuthServer.ts +++ b/src/scenarios/client/auth/helpers/createAuthServer.ts @@ -10,16 +10,21 @@ export interface AuthServerOptions { loggingEnabled?: boolean; routePrefix?: string; scopesSupported?: string[]; + tokenEndpointAuthMethodsSupported?: string[]; tokenVerifier?: MockTokenVerifier; - onTokenRequest?: (requestData: { - scope?: string; - grantType: string; - timestamp: string; - }) => { token: string; scopes: string[] }; + onTokenRequest?: ( + req: Request, + timestamp: string + ) => { token: string; scopes: string[] } | void; onAuthorizationRequest?: (requestData: { scope?: string; timestamp: string; }) => void; + onRegistrationRequest?: (req: Request) => { + clientId: string; + clientSecret?: string; + tokenEndpointAuthMethod?: string; + }; } export function createAuthServer( @@ -33,9 +38,11 @@ export function createAuthServer( loggingEnabled = true, routePrefix = '', scopesSupported, + tokenEndpointAuthMethodsSupported = ['none'], tokenVerifier, onTokenRequest, - onAuthorizationRequest + onAuthorizationRequest, + onRegistrationRequest } = options; // Track scopes from the most recent authorization request @@ -85,7 +92,7 @@ export function createAuthServer( response_types_supported: ['code'], grant_types_supported: ['authorization_code', 'refresh_token'], code_challenge_methods_supported: ['S256'], - token_endpoint_auth_methods_supported: ['none'] + token_endpoint_auth_methods_supported: tokenEndpointAuthMethodsSupported }; // Add scopes_supported if provided @@ -141,7 +148,6 @@ export function createAuthServer( app.post(authRoutes.token_endpoint, (req: Request, res: Response) => { const timestamp = new Date().toISOString(); - const requestedScope = req.body.scope; checks.push({ id: 'token-request', @@ -160,13 +166,11 @@ export function createAuthServer( let scopes: string[] = lastAuthorizationScopes; if (onTokenRequest) { - const result = onTokenRequest({ - scope: requestedScope, - grantType: req.body.grant_type, - timestamp - }); - token = result.token; - scopes = result.scopes; + const result = onTokenRequest(req, timestamp); + if (result) { + token = result.token; + scopes = result.scopes; + } } // Register token with verifier if provided @@ -183,6 +187,17 @@ export function createAuthServer( }); app.post(authRoutes.registration_endpoint, (req: Request, res: Response) => { + let clientId = 'test-client-id'; + let clientSecret: string | undefined = 'test-client-secret'; + let tokenEndpointAuthMethod: string | undefined; + + if (onRegistrationRequest) { + const result = onRegistrationRequest(req); + clientId = result.clientId; + clientSecret = result.clientSecret; + tokenEndpointAuthMethod = result.tokenEndpointAuthMethod; + } + checks.push({ id: 'client-registration', name: 'ClientRegistration', @@ -192,15 +207,19 @@ export function createAuthServer( specReferences: [SpecReferences.MCP_DCR], details: { endpoint: '/register', - clientName: req.body.client_name + clientName: req.body.client_name, + ...(tokenEndpointAuthMethod && { tokenEndpointAuthMethod }) } }); res.status(201).json({ - client_id: 'test-client-id', - client_secret: 'test-client-secret', + client_id: clientId, + ...(clientSecret && { client_secret: clientSecret }), client_name: req.body.client_name || 'test-client', - redirect_uris: req.body.redirect_uris || [] + redirect_uris: req.body.redirect_uris || [], + ...(tokenEndpointAuthMethod && { + token_endpoint_auth_method: tokenEndpointAuthMethod + }) }); }); diff --git a/src/scenarios/client/auth/token-endpoint-auth.ts b/src/scenarios/client/auth/token-endpoint-auth.ts index 872a09e..976f69b 100644 --- a/src/scenarios/client/auth/token-endpoint-auth.ts +++ b/src/scenarios/client/auth/token-endpoint-auth.ts @@ -1,25 +1,13 @@ import type { Scenario, ConformanceCheck } from '../../../types.js'; import { ScenarioUrls } from '../../../types.js'; +import { createAuthServer } from './helpers/createAuthServer.js'; import { createServer } from './helpers/createServer.js'; import { ServerLifecycle } from './helpers/serverLifecycle.js'; import { SpecReferences } from './spec-references.js'; import { MockTokenVerifier } from './helpers/mockTokenVerifier.js'; -import { createRequestLogger } from '../../request-logger.js'; -import express, { Request, Response } from 'express'; type AuthMethod = 'client_secret_basic' | 'client_secret_post' | 'none'; -interface AuthServerOptions { - tokenVerifier?: MockTokenVerifier; - tokenEndpointAuthMethodsSupported: string[]; - expectedAuthMethod: AuthMethod; - onTokenRequest?: (requestData: { - authorizationHeader?: string; - bodyClientSecret?: string; - timestamp: string; - }) => void; -} - function detectAuthMethod( authorizationHeader?: string, bodyClientSecret?: string @@ -49,156 +37,6 @@ function validateBasicAuthFormat(authorizationHeader: string): { } } -function createAuthServerForTokenAuth( - checks: ConformanceCheck[], - getAuthBaseUrl: () => string, - options: AuthServerOptions -): express.Application { - const { - tokenVerifier, - tokenEndpointAuthMethodsSupported, - expectedAuthMethod, - onTokenRequest - } = options; - - const authRoutes = { - authorization_endpoint: '/authorize', - token_endpoint: '/token', - registration_endpoint: '/register' - }; - - const app = express(); - app.use(express.json()); - app.use(express.urlencoded({ extended: true })); - - app.use( - createRequestLogger(checks, { - incomingId: 'incoming-auth-request', - outgoingId: 'outgoing-auth-response' - }) - ); - - app.get( - '/.well-known/oauth-authorization-server', - (req: Request, res: Response) => { - checks.push({ - id: 'authorization-server-metadata', - name: 'AuthorizationServerMetadata', - description: 'Client requested authorization server metadata', - status: 'SUCCESS', - timestamp: new Date().toISOString(), - specReferences: [ - SpecReferences.RFC_AUTH_SERVER_METADATA_REQUEST, - SpecReferences.MCP_AUTH_DISCOVERY - ], - details: { - url: req.url, - path: req.path - } - }); - - res.json({ - issuer: getAuthBaseUrl(), - authorization_endpoint: `${getAuthBaseUrl()}${authRoutes.authorization_endpoint}`, - token_endpoint: `${getAuthBaseUrl()}${authRoutes.token_endpoint}`, - registration_endpoint: `${getAuthBaseUrl()}${authRoutes.registration_endpoint}`, - response_types_supported: ['code'], - grant_types_supported: ['authorization_code', 'refresh_token'], - code_challenge_methods_supported: ['S256'], - token_endpoint_auth_methods_supported: tokenEndpointAuthMethodsSupported - }); - } - ); - - app.get(authRoutes.authorization_endpoint, (req: Request, res: Response) => { - checks.push({ - id: 'authorization-request', - name: 'AuthorizationRequest', - description: 'Client made authorization request', - status: 'SUCCESS', - timestamp: new Date().toISOString(), - specReferences: [SpecReferences.OAUTH_2_1_AUTHORIZATION_ENDPOINT], - details: { query: req.query } - }); - - const redirectUri = req.query.redirect_uri as string; - const state = req.query.state as string; - const redirectUrl = new URL(redirectUri); - redirectUrl.searchParams.set('code', 'test-auth-code'); - if (state) { - redirectUrl.searchParams.set('state', state); - } - - res.redirect(redirectUrl.toString()); - }); - - app.post(authRoutes.token_endpoint, (req: Request, res: Response) => { - const timestamp = new Date().toISOString(); - const authorizationHeader = req.headers.authorization as string | undefined; - const bodyClientSecret = req.body.client_secret; - - checks.push({ - id: 'token-request', - name: 'TokenRequest', - description: 'Client requested access token', - status: 'SUCCESS', - timestamp, - specReferences: [SpecReferences.OAUTH_2_1_TOKEN], - details: { - endpoint: '/token', - grantType: req.body.grant_type, - hasAuthorizationHeader: !!authorizationHeader, - hasBodyClientSecret: !!bodyClientSecret - } - }); - - if (onTokenRequest) { - onTokenRequest({ authorizationHeader, bodyClientSecret, timestamp }); - } - - const token = `test-token-${Date.now()}`; - if (tokenVerifier) { - tokenVerifier.registerToken(token, []); - } - - res.json({ - access_token: token, - token_type: 'Bearer', - expires_in: 3600 - }); - }); - - app.post(authRoutes.registration_endpoint, (req: Request, res: Response) => { - const clientId = `test-client-${Date.now()}`; - const clientSecret = - expectedAuthMethod === 'none' ? undefined : `test-secret-${Date.now()}`; - - checks.push({ - id: 'client-registration', - name: 'ClientRegistration', - description: 'Client registered with authorization server', - status: 'SUCCESS', - timestamp: new Date().toISOString(), - specReferences: [SpecReferences.MCP_DCR], - details: { - endpoint: '/register', - clientName: req.body.client_name, - tokenEndpointAuthMethod: expectedAuthMethod - } - }); - - res.status(201).json({ - client_id: clientId, - ...(clientSecret && { client_secret: clientSecret }), - client_name: req.body.client_name || 'test-client', - redirect_uris: req.body.redirect_uris || [], - token_endpoint_auth_method: expectedAuthMethod - }); - }); - - return app; -} - const AUTH_METHOD_NAMES: Record = { client_secret_basic: 'HTTP Basic authentication (client_secret_basic)', client_secret_post: 'client_secret_post', @@ -223,63 +61,65 @@ class TokenEndpointAuthScenario implements Scenario { this.checks = []; const tokenVerifier = new MockTokenVerifier(this.checks, []); - const authApp = createAuthServerForTokenAuth( - this.checks, - this.authServer.getUrl, - { - tokenVerifier, - tokenEndpointAuthMethodsSupported: [this.expectedAuthMethod], - expectedAuthMethod: this.expectedAuthMethod, - onTokenRequest: (data) => { - const actualMethod = detectAuthMethod( - data.authorizationHeader, - data.bodyClientSecret - ); - const isCorrect = actualMethod === this.expectedAuthMethod; - - // For basic auth, also validate the format - let formatError: string | undefined; - if ( - actualMethod === 'client_secret_basic' && - data.authorizationHeader - ) { - const validation = validateBasicAuthFormat( - data.authorizationHeader - ); - if (!validation.valid) { - formatError = validation.error; - } + const authApp = createAuthServer(this.checks, this.authServer.getUrl, { + tokenVerifier, + tokenEndpointAuthMethodsSupported: [this.expectedAuthMethod], + onTokenRequest: (req, timestamp) => { + const authorizationHeader = req.headers.authorization as + | string + | undefined; + const bodyClientSecret = req.body.client_secret; + const actualMethod = detectAuthMethod( + authorizationHeader, + bodyClientSecret + ); + const isCorrect = actualMethod === this.expectedAuthMethod; + + // For basic auth, also validate the format + let formatError: string | undefined; + if (actualMethod === 'client_secret_basic' && authorizationHeader) { + const validation = validateBasicAuthFormat(authorizationHeader); + if (!validation.valid) { + formatError = validation.error; } + } - const status = isCorrect && !formatError ? 'SUCCESS' : 'FAILURE'; - let description: string; - - if (formatError) { - description = `Client sent Basic auth header but ${formatError}`; - } else if (isCorrect) { - description = `Client correctly used ${AUTH_METHOD_NAMES[this.expectedAuthMethod]} for token endpoint`; - } else { - description = `Client used ${actualMethod} but server only supports ${this.expectedAuthMethod}`; - } + const status = isCorrect && !formatError ? 'SUCCESS' : 'FAILURE'; + let description: string; - this.checks.push({ - id: 'token-endpoint-auth-method', - name: 'Token endpoint authentication method', - description, - status, - timestamp: data.timestamp, - specReferences: [SpecReferences.OAUTH_2_1_TOKEN], - details: { - expectedAuthMethod: this.expectedAuthMethod, - actualAuthMethod: actualMethod, - hasAuthorizationHeader: !!data.authorizationHeader, - hasBodyClientSecret: !!data.bodyClientSecret, - ...(formatError && { formatError }) - } - }); + if (formatError) { + description = `Client sent Basic auth header but ${formatError}`; + } else if (isCorrect) { + description = `Client correctly used ${AUTH_METHOD_NAMES[this.expectedAuthMethod]} for token endpoint`; + } else { + description = `Client used ${actualMethod} but server only supports ${this.expectedAuthMethod}`; } - } - ); + + this.checks.push({ + id: 'token-endpoint-auth-method', + name: 'Token endpoint authentication method', + description, + status, + timestamp, + specReferences: [SpecReferences.OAUTH_2_1_TOKEN], + details: { + expectedAuthMethod: this.expectedAuthMethod, + actualAuthMethod: actualMethod, + hasAuthorizationHeader: !!authorizationHeader, + hasBodyClientSecret: !!bodyClientSecret, + ...(formatError && { formatError }) + } + }); + }, + onRegistrationRequest: () => ({ + clientId: `test-client-${Date.now()}`, + clientSecret: + this.expectedAuthMethod === 'none' + ? undefined + : `test-secret-${Date.now()}`, + tokenEndpointAuthMethod: this.expectedAuthMethod + }) + }); await this.authServer.start(authApp); const app = createServer( From 038fecb7738791201f1da5f054a5d1a6e47c7352 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Mon, 5 Jan 2026 14:23:20 +0000 Subject: [PATCH 5/6] Fix onTokenRequest callback to use new interface The createAuthServer onTokenRequest callback now passes an object with authorizationHeader, body, timestamp etc. instead of (req, timestamp). Also added missing return value for TokenRequestResult. --- src/scenarios/client/auth/token-endpoint-auth.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/scenarios/client/auth/token-endpoint-auth.ts b/src/scenarios/client/auth/token-endpoint-auth.ts index 976f69b..4203789 100644 --- a/src/scenarios/client/auth/token-endpoint-auth.ts +++ b/src/scenarios/client/auth/token-endpoint-auth.ts @@ -64,11 +64,8 @@ class TokenEndpointAuthScenario implements Scenario { const authApp = createAuthServer(this.checks, this.authServer.getUrl, { tokenVerifier, tokenEndpointAuthMethodsSupported: [this.expectedAuthMethod], - onTokenRequest: (req, timestamp) => { - const authorizationHeader = req.headers.authorization as - | string - | undefined; - const bodyClientSecret = req.body.client_secret; + onTokenRequest: ({ authorizationHeader, body, timestamp }) => { + const bodyClientSecret = body.client_secret; const actualMethod = detectAuthMethod( authorizationHeader, bodyClientSecret @@ -110,6 +107,11 @@ class TokenEndpointAuthScenario implements Scenario { ...(formatError && { formatError }) } }); + + return { + token: `test-token-${Date.now()}`, + scopes: [] + }; }, onRegistrationRequest: () => ({ clientId: `test-client-${Date.now()}`, From 005f1a8ff76844fa735cb0e465dc4a33ef4285e8 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Mon, 5 Jan 2026 14:28:08 +0000 Subject: [PATCH 6/6] Update @modelcontextprotocol/sdk to 1.25.1 --- package-lock.json | 51 +++++++++++++++++++++++++++++++++++++---------- package.json | 2 +- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index c8fe2e6..2df67dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.8", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.23.0-beta.0", + "@modelcontextprotocol/sdk": "^1.25.1", "commander": "^14.0.2", "eventsource-parser": "^3.0.6", "express": "^5.1.0", @@ -749,6 +749,18 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@hono/node-server": { + "version": "1.19.7", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz", + "integrity": "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -841,11 +853,12 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.23.0-beta.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.23.0-beta.0.tgz", - "integrity": "sha512-NvoyrhhNcNiyf0nI8J1O+wheNiyOzK3kMTkMuwGb/TGHpSHXCcubcg0IxC/p9Aym+K4QZFxq9Wn67clOAegFKQ==", + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz", + "integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==", "license": "MIT", "dependencies": { + "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", @@ -855,6 +868,8 @@ "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", + "jose": "^6.1.1", + "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", @@ -3429,6 +3444,16 @@ "node": ">= 0.4" } }, + "node_modules/hono": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.3.tgz", + "integrity": "sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/hookable": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", @@ -3629,6 +3654,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -4167,9 +4198,9 @@ } }, "node_modules/pkce-challenge": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", - "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", "license": "MIT", "engines": { "node": ">=16.20.0" @@ -5314,9 +5345,9 @@ } }, "node_modules/zod-to-json-schema": { - "version": "3.25.0", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz", - "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==", + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", "license": "ISC", "peerDependencies": { "zod": "^3.25 || ^4" diff --git a/package.json b/package.json index fc781d9..55aa1c4 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "vitest": "^4.0.5" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.23.0-beta.0", + "@modelcontextprotocol/sdk": "^1.25.1", "commander": "^14.0.2", "eventsource-parser": "^3.0.6", "express": "^5.1.0",