Skip to content
Next Next commit
First pass at adding an auth test scenario (basic) (Opus 4.5)
  • Loading branch information
tobinsouth committed Nov 26, 2025
commit d7f90befe1e0654fcf1972005b9ac9eeae99a228
35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,30 @@ npx @modelcontextprotocol/conformance server --url <url> [--scenario <scenario>]
**Options:**

- `--url` - URL of the server to test
- `--scenario <scenario>` - Test scenario to run (e.g., "server-initialize". Runs all available scenarios by default
- `--scenario <scenario>` - Test scenario to run (e.g., "server-initialize"). Runs all available scenarios by default
- `--suite <suite>` - Suite to run: "active" (default), "all", "pending", or "auth"
- `--auth` - Include OAuth conformance tests when running active suite
- `--verbose` - Show verbose output (JSON format)

### Server OAuth Testing

To test OAuth implementation on your server:

```bash
# Run only OAuth conformance tests
npx @modelcontextprotocol/conformance server --url http://localhost:3000/mcp --suite auth

# Run active tests plus OAuth tests
npx @modelcontextprotocol/conformance server --url http://localhost:3000/mcp --auth

# Run a specific OAuth scenario
npx @modelcontextprotocol/conformance server --url http://localhost:3000/mcp --scenario server/auth-prm-discovery
```

OAuth scenarios validate:
- **server/auth-prm-discovery** - Protected Resource Metadata endpoint
- **server/auth-401-unauthorized** - 401 response for unauthenticated requests
- **server/auth-www-authenticate-header** - WWW-Authenticate header format

## Test Results

Expand Down Expand Up @@ -114,6 +137,16 @@ Run `npx @modelcontextprotocol/conformance list --server` to see all available s
- **resources-\*** - Resource management scenarios
- **prompts-\*** - Prompt management scenarios

### Server OAuth Scenarios

Run `npx @modelcontextprotocol/conformance list --auth` to see OAuth scenarios:

- **server/auth-prm-discovery** - Validates Protected Resource Metadata endpoint per RFC 9728
- **server/auth-401-unauthorized** - Validates 401 response for unauthenticated requests
- **server/auth-www-authenticate-header** - Validates WWW-Authenticate header format per RFC 6750

These scenarios test OAuth compliance without requiring a valid access token, making them suitable for basic conformance testing of any OAuth-protected MCP server.

## Architecture

See `src/runner/DESIGN.md` for detailed architecture documentation.
Expand Down
37 changes: 30 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import {
listActiveClientScenarios,
listPendingClientScenarios,
listAuthScenarios,
listMetadataScenarios
listMetadataScenarios,
listServerAuthScenarios
} from './scenarios';
import { ConformanceCheck } from './types';
import { ClientOptionsSchema, ServerOptionsSchema } from './schemas';
Expand Down Expand Up @@ -201,9 +202,10 @@ program
)
.option(
'--suite <suite>',
'Suite to run: "active" (default, excludes pending), "all", or "pending"',
'Suite to run: "active" (default), "all", "pending", or "auth"',
'active'
)
.option('--auth', 'Include OAuth conformance tests (server/auth-* scenarios)')
.option('--verbose', 'Show verbose output (JSON instead of pretty print)')
.action(async (options) => {
try {
Expand All @@ -228,22 +230,31 @@ program
} else {
// Run scenarios based on suite
const suite = options.suite?.toLowerCase() || 'active';
const includeAuth = options.auth ?? false;
let scenarios: string[];

if (suite === 'all') {
scenarios = listClientScenarios();
} else if (suite === 'active') {
scenarios = listActiveClientScenarios();
// Add auth scenarios if --auth flag is set
if (includeAuth) {
scenarios = [...scenarios, ...listServerAuthScenarios()];
}
} else if (suite === 'pending') {
scenarios = listPendingClientScenarios();
} else if (suite === 'auth') {
// Run only auth scenarios
scenarios = listServerAuthScenarios();
} else {
console.error(`Unknown suite: ${suite}`);
console.error('Available suites: active, all, pending');
console.error('Available suites: active, all, pending, auth');
process.exit(1);
}

const authNote = includeAuth && suite !== 'auth' ? ' (with auth)' : '';
console.log(
`Running ${suite} suite (${scenarios.length} scenarios) against ${validated.url}\n`
`Running ${suite}${authNote} suite (${scenarios.length} scenarios) against ${validated.url}\n`
);

const allResults: { scenario: string; checks: ConformanceCheck[] }[] =
Expand Down Expand Up @@ -300,15 +311,27 @@ program
.description('List available test scenarios')
.option('--client', 'List client scenarios')
.option('--server', 'List server scenarios')
.option('--auth', 'List server OAuth auth scenarios')
.action((options) => {
if (options.server || (!options.client && !options.server)) {
const showAll = !options.client && !options.server && !options.auth;

if (options.server || showAll) {
console.log('Server scenarios (test against a server):');
const serverScenarios = listClientScenarios();
serverScenarios.forEach((s) => console.log(` - ${s}`));
}

if (options.client || (!options.client && !options.server)) {
if (options.server || (!options.client && !options.server)) {
if (options.auth || showAll) {
if (options.server || showAll) {
console.log('');
}
console.log('Server OAuth scenarios (use --auth or --suite auth):');
const authScenarios = listServerAuthScenarios();
authScenarios.forEach((s) => console.log(` - ${s}`));
}

if (options.client || showAll) {
if (options.server || options.auth || showAll) {
console.log('');
}
console.log('Client scenarios (test against a client):');
Expand Down
17 changes: 15 additions & 2 deletions src/scenarios/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ import {
import { authScenariosList } from './client/auth/index';
import { listMetadataScenarios } from './client/auth/discovery-metadata';

// Server auth scenarios (OAuth conformance testing)
import {
serverAuthScenarios,
listServerAuthScenarios,
getServerAuthScenario
} from './server/auth/index';

// Pending client scenarios (not yet fully tested/implemented)
const pendingClientScenariosList: ClientScenario[] = [
// Elicitation scenarios (SEP-1330)
Expand Down Expand Up @@ -133,9 +140,12 @@ const activeClientScenariosList: ClientScenario[] =
)
);

// Client scenarios map - built from list
// Client scenarios map - built from list (includes server auth scenarios)
export const clientScenarios = new Map<string, ClientScenario>(
allClientScenariosList.map((scenario) => [scenario.name, scenario])
[...allClientScenariosList, ...serverAuthScenarios].map((scenario) => [
scenario.name,
scenario
])
);

// Scenario scenarios
Expand Down Expand Up @@ -185,3 +195,6 @@ export function listAuthScenarios(): string[] {
}

export { listMetadataScenarios };

// Server auth scenario exports
export { listServerAuthScenarios, getServerAuthScenario, serverAuthScenarios };
Loading