Skip to content
Merged
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
8 changes: 4 additions & 4 deletions examples/clients/typescript/auth-test-bad-prm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import {
UnauthorizedError
} from '@modelcontextprotocol/sdk/client/auth.js';
import type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js';
import { withOAuthRetry } from './helpers/withOAuthRetry.js';
import { ConformanceOAuthProvider } from './helpers/ConformanceOAuthProvider.js';
import { runAsCli } from './helpers/cliRunner.js';
import { logger } from './helpers/logger.js';
import { withOAuthRetry } from './helpers/withOAuthRetry';
import { ConformanceOAuthProvider } from './helpers/ConformanceOAuthProvider';
import { runAsCli } from './helpers/cliRunner';
import { logger } from './helpers/logger';

/**
* Broken client that always uses root-based PRM discovery.
Expand Down
8 changes: 4 additions & 4 deletions examples/clients/typescript/auth-test-ignore-403.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import {
UnauthorizedError
} from '@modelcontextprotocol/sdk/client/auth.js';
import type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js';
import { withOAuthRetry } from './helpers/withOAuthRetry.js';
import { ConformanceOAuthProvider } from './helpers/ConformanceOAuthProvider.js';
import { runAsCli } from './helpers/cliRunner.js';
import { logger } from './helpers/logger.js';
import { withOAuthRetry } from './helpers/withOAuthRetry';
import { ConformanceOAuthProvider } from './helpers/ConformanceOAuthProvider';
import { runAsCli } from './helpers/cliRunner';
import { logger } from './helpers/logger';

/**
* Broken client that only responds to 401, not 403.
Expand Down
8 changes: 4 additions & 4 deletions examples/clients/typescript/auth-test-ignore-scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import {
UnauthorizedError
} from '@modelcontextprotocol/sdk/client/auth.js';
import type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js';
import { withOAuthRetry } from './helpers/withOAuthRetry.js';
import { ConformanceOAuthProvider } from './helpers/ConformanceOAuthProvider.js';
import { runAsCli } from './helpers/cliRunner.js';
import { logger } from './helpers/logger.js';
import { withOAuthRetry } from './helpers/withOAuthRetry';
import { ConformanceOAuthProvider } from './helpers/ConformanceOAuthProvider';
import { runAsCli } from './helpers/cliRunner';
import { logger } from './helpers/logger';

/**
* Broken client that ignores the scope from WWW-Authenticate header.
Expand Down
51 changes: 51 additions & 0 deletions examples/clients/typescript/auth-test-no-cimd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env node

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { withOAuthRetry } from './helpers/withOAuthRetry';
import { runAsCli } from './helpers/cliRunner';
import { logger } from './helpers/logger';

/**
* Non-compliant client that doesn't use CIMD (Client ID Metadata Document).
*
* This client intentionally omits the clientMetadataUrl parameter when the server
* advertises client_id_metadata_document_supported=true. A compliant client should
* use CIMD when the server supports it, but this client falls back to DCR (Dynamic
* Client Registration) instead.
*
* Used to test that conformance checks detect clients that don't properly
* implement CIMD support.
*/
export async function runClient(serverUrl: string): Promise<void> {
const client = new Client(
{ name: 'test-auth-client-no-cimd', version: '1.0.0' },
{ capabilities: {} }
);

// Non-compliant: omitting clientMetadataUrl causes fallback to DCR
// A compliant client would pass a clientMetadataUrl here when the server
// advertises client_id_metadata_document_supported=true
const oauthFetch = withOAuthRetry(
'test-auth-client-no-cimd',
new URL(serverUrl)
)(fetch);

const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
fetch: oauthFetch
});

await client.connect(transport);
logger.debug('Connected to MCP server (without CIMD)');

await client.listTools();
logger.debug('Successfully listed tools');

await client.callTool({ name: 'test-tool', arguments: {} });
logger.debug('Successfully called tool');

await transport.close();
logger.debug('Connection closed successfully');
}

runAsCli(runClient, import.meta.url, 'auth-test-no-cimd <server-url>');
8 changes: 4 additions & 4 deletions examples/clients/typescript/auth-test-partial-scopes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import {
UnauthorizedError
} from '@modelcontextprotocol/sdk/client/auth.js';
import type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js';
import { withOAuthRetry } from './helpers/withOAuthRetry.js';
import { ConformanceOAuthProvider } from './helpers/ConformanceOAuthProvider.js';
import { runAsCli } from './helpers/cliRunner.js';
import { logger } from './helpers/logger.js';
import { withOAuthRetry } from './helpers/withOAuthRetry';
import { ConformanceOAuthProvider } from './helpers/ConformanceOAuthProvider';
import { runAsCli } from './helpers/cliRunner';
import { logger } from './helpers/logger';

/**
* Broken client that only requests a subset of scopes.
Expand Down
18 changes: 14 additions & 4 deletions examples/clients/typescript/auth-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { withOAuthRetry } from './helpers/withOAuthRetry.js';
import { runAsCli } from './helpers/cliRunner.js';
import { logger } from './helpers/logger.js';
import { withOAuthRetry, handle401 } from './helpers/withOAuthRetry';
import { runAsCli } from './helpers/cliRunner';
import { logger } from './helpers/logger';

/**
* Fixed client metadata URL for CIMD conformance tests.
* When server supports client_id_metadata_document_supported, this URL
* will be used as the client_id instead of doing dynamic registration.
*/
const CIMD_CLIENT_METADATA_URL =
'https://conformance-test.local/client-metadata.json';

/**
* Well-behaved auth client that follows all OAuth protocols correctly.
Expand All @@ -17,7 +25,9 @@ export async function runClient(serverUrl: string): Promise<void> {

const oauthFetch = withOAuthRetry(
'test-auth-client',
new URL(serverUrl)
new URL(serverUrl),
handle401,
CIMD_CLIENT_METADATA_URL
)(fetch);

const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
Expand Down
10 changes: 4 additions & 6 deletions examples/clients/typescript/helpers/ConformanceOAuthProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,11 @@ export class ConformanceOAuthProvider implements OAuthClientProvider {
return this._clientMetadata;
}

get clientMetadataUrl(): string | undefined {
return this._clientMetadataUrl?.toString();
}

clientInformation(): OAuthClientInformation | undefined {
if (this._clientMetadataUrl) {
console.log('Using client ID metadata URL');
return {
client_id: this._clientMetadataUrl.toString()
};
}
return this._clientInformation;
}

Expand Down
6 changes: 4 additions & 2 deletions examples/clients/typescript/helpers/withOAuthRetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,16 @@ export const handle401 = async (
export const withOAuthRetry = (
clientName: string,
baseUrl?: string | URL,
handle401Fn: typeof handle401 = handle401
handle401Fn: typeof handle401 = handle401,
clientMetadataUrl?: string
): Middleware => {
const provider = new ConformanceOAuthProvider(
'http://localhost:3000/callback',
{
client_name: clientName,
redirect_uris: ['http://localhost:3000/callback']
}
},
clientMetadataUrl
);
return (next: FetchLike) => {
return async (
Expand Down
2 changes: 1 addition & 1 deletion src/checks/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ConformanceCheck, CheckStatus } from '../types.js';
import { ConformanceCheck, CheckStatus } from '../types';

export function createServerInfoCheck(serverInfo: {
name: string;
Expand Down
2 changes: 1 addition & 1 deletion src/checks/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Namespaced exports for client checks
import * as client from './client.js';
import * as client from './client';

export const clientChecks = client;
99 changes: 99 additions & 0 deletions src/scenarios/client/auth/basic-cimd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import type { Scenario, ConformanceCheck } from '../../../types';
import { ScenarioUrls } from '../../../types';
import { createAuthServer } from './helpers/createAuthServer';
import { createServer } from './helpers/createServer';
import { ServerLifecycle } from './helpers/serverLifecycle';
import { SpecReferences } from './spec-references';

/**
* Fixed client metadata URL that clients should use for CIMD tests.
* This URL doesn't need to resolve - the server will accept it as-is
* and use hardcoded metadata.
*/
export const CIMD_CLIENT_METADATA_URL =
'https://conformance-test.local/client-metadata.json';

/**
* Scenario: Client ID Metadata Documents (SEP-991/URL-based client IDs)
*
* Tests that when a server advertises client_id_metadata_document_supported=true,
* clients SHOULD use a URL as their client_id instead of using dynamic client
* registration.
*/
export class AuthBasicCIMDScenario implements Scenario {
name = 'auth/basic-cimd';
description =
'Tests OAuth flow with Client ID Metadata Documents (SEP-991/URL-based client IDs). Server advertises client_id_metadata_document_supported=true and client should use URL as client_id instead of DCR.';
private authServer = new ServerLifecycle();
private server = new ServerLifecycle();
private checks: ConformanceCheck[] = [];

async start(): Promise<ScenarioUrls> {
this.checks = [];

const authApp = createAuthServer(this.checks, this.authServer.getUrl, {
clientIdMetadataDocumentSupported: true,
onAuthorizationRequest: (data) => {
// Check if client used URL-based client ID
const usedUrlClientId = data.clientId === CIMD_CLIENT_METADATA_URL;
this.checks.push({
id: 'cimd-client-id-used',
name: 'Client ID Metadata Document Usage',
description: usedUrlClientId
? 'Client correctly used URL-based client ID when server supports client_id_metadata_document_supported'
: 'Client SHOULD use URL-based client ID when server advertises client_id_metadata_document_supported=true',
status: usedUrlClientId ? 'SUCCESS' : 'WARNING',
timestamp: data.timestamp,
specReferences: [
SpecReferences.MCP_CLIENT_ID_METADATA_DOCUMENTS,
SpecReferences.IETF_CIMD
],
details: {
expectedClientId: CIMD_CLIENT_METADATA_URL,
actualClientId: data.clientId || 'none'
}
});
}
});

await this.authServer.start(authApp);

const app = createServer(
this.checks,
this.server.getUrl,
this.authServer.getUrl
);

await this.server.start(app);

return { serverUrl: `${this.server.getUrl()}/mcp` };
}

async stop() {
await this.authServer.stop();
await this.server.stop();
}

getChecks(): ConformanceCheck[] {
// Ensure we have the CIMD check - if not, the client didn't make an auth request
const hasCimdCheck = this.checks.some(
(c) => c.id === 'cimd-client-id-used'
);
if (!hasCimdCheck) {
this.checks.push({
id: 'cimd-client-id-used',
name: 'Client ID Metadata Document Usage',
description:
'Client did not make an authorization request to test CIMD support',
status: 'FAILURE',
timestamp: new Date().toISOString(),
specReferences: [
SpecReferences.MCP_CLIENT_ID_METADATA_DOCUMENTS,
SpecReferences.IETF_CIMD
]
});
}

return this.checks;
}
}
12 changes: 6 additions & 6 deletions src/scenarios/client/auth/discovery-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
* generated from them.
*/

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 type { Scenario, ConformanceCheck } from '../../../types';
import { ScenarioUrls } from '../../../types';
import { createAuthServer } from './helpers/createAuthServer';
import { createServer } from './helpers/createServer';
import { ServerLifecycle } from './helpers/serverLifecycle';
import { SpecReferences } from './spec-references';
import { Request, Response } from 'express';

/**
Expand Down
18 changes: 14 additions & 4 deletions src/scenarios/client/auth/helpers/createAuthServer.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import express, { Request, Response } from 'express';
import type { ConformanceCheck } from '../../../../types.js';
import { createRequestLogger } from '../../../request-logger.js';
import { SpecReferences } from '../spec-references.js';
import { MockTokenVerifier } from './mockTokenVerifier.js';
import type { ConformanceCheck } from '../../../../types';
import { createRequestLogger } from '../../../request-logger';
import { SpecReferences } from '../spec-references';
import { MockTokenVerifier } from './mockTokenVerifier';

export interface AuthServerOptions {
metadataPath?: string;
isOpenIdConfiguration?: boolean;
loggingEnabled?: boolean;
routePrefix?: string;
scopesSupported?: string[];
clientIdMetadataDocumentSupported?: boolean;
tokenVerifier?: MockTokenVerifier;
onTokenRequest?: (requestData: {
scope?: string;
grantType: string;
timestamp: string;
}) => { token: string; scopes: string[] };
onAuthorizationRequest?: (requestData: {
clientId?: string;
scope?: string;
timestamp: string;
}) => void;
Expand All @@ -33,6 +35,7 @@ export function createAuthServer(
loggingEnabled = true,
routePrefix = '',
scopesSupported,
clientIdMetadataDocumentSupported,
tokenVerifier,
onTokenRequest,
onAuthorizationRequest
Expand Down Expand Up @@ -93,6 +96,12 @@ export function createAuthServer(
metadata.scopes_supported = scopesSupported;
}

// Add client_id_metadata_document_supported if provided
if (clientIdMetadataDocumentSupported !== undefined) {
metadata.client_id_metadata_document_supported =
clientIdMetadataDocumentSupported;
}

// Add OpenID Configuration specific fields
if (isOpenIdConfiguration) {
metadata.jwks_uri = `${getAuthBaseUrl()}/.well-known/jwks.json`;
Expand Down Expand Up @@ -123,6 +132,7 @@ export function createAuthServer(

if (onAuthorizationRequest) {
onAuthorizationRequest({
clientId: req.query.client_id as string | undefined,
scope: scopeParam,
timestamp
});
Expand Down
8 changes: 4 additions & 4 deletions src/scenarios/client/auth/helpers/createServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import {
} from '@modelcontextprotocol/sdk/types.js';
import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
import express, { Request, Response, NextFunction } from 'express';
import type { ConformanceCheck } from '../../../../types.js';
import { createRequestLogger } from '../../../request-logger.js';
import { MockTokenVerifier } from './mockTokenVerifier.js';
import { SpecReferences } from '../spec-references.js';
import type { ConformanceCheck } from '../../../../types';
import { createRequestLogger } from '../../../request-logger';
import { MockTokenVerifier } from './mockTokenVerifier';
import { SpecReferences } from '../spec-references';

export interface ServerOptions {
prmPath?: string | null;
Expand Down
4 changes: 2 additions & 2 deletions src/scenarios/client/auth/helpers/mockTokenVerifier.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { OAuthTokenVerifier } from '@modelcontextprotocol/sdk/server/auth/provider.js';
import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
import type { ConformanceCheck } from '../../../../types.js';
import { SpecReferences } from '../spec-references.js';
import type { ConformanceCheck } from '../../../../types';
import { SpecReferences } from '../spec-references';

export class MockTokenVerifier implements OAuthTokenVerifier {
private tokenScopes: Map<string, string[]> = new Map();
Expand Down
Loading
Loading