Skip to content
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,20 @@ 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

### Authorization Server OAuth Conformity Testing

To test the OAuth implementation protecting your server:

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

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

## Test Results

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
15 changes: 14 additions & 1 deletion 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 @@ -135,7 +142,10 @@ const activeClientScenariosList: ClientScenario[] =

// Client scenarios map - built from list
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 };
72 changes: 72 additions & 0 deletions src/scenarios/server/auth/helpers/as-metadata.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { describe, expect, test } from 'vitest';

import { buildAsMetadataDiscoveryAttempts } from './as-metadata';

describe('buildAsMetadataDiscoveryAttempts', () => {
test('issuer without path components: tries RFC8414 then OIDC at root well-known endpoints', () => {
const attempts = buildAsMetadataDiscoveryAttempts(
'https://auth.example.com'
);

expect(attempts).toEqual([
{
kind: 'RFC8414',
variant: 'root',
url: 'https://auth.example.com/.well-known/oauth-authorization-server'
},
{
kind: 'OIDC',
variant: 'root',
url: 'https://auth.example.com/.well-known/openid-configuration'
}
]);
});

test('issuer with path components: tries RFC8414 path-insert, OIDC path-insert, then OIDC path-append', () => {
const attempts = buildAsMetadataDiscoveryAttempts(
'https://auth.example.com/tenant1'
);

expect(attempts).toEqual([
{
kind: 'RFC8414',
variant: 'path-insert',
url: 'https://auth.example.com/.well-known/oauth-authorization-server/tenant1'
},
{
kind: 'OIDC',
variant: 'path-insert',
url: 'https://auth.example.com/.well-known/openid-configuration/tenant1'
},
{
kind: 'OIDC',
variant: 'path-append',
url: 'https://auth.example.com/tenant1/.well-known/openid-configuration'
}
]);
});

test('issuer with trailing slash: path-append normalizes to avoid double slashes', () => {
const attempts = buildAsMetadataDiscoveryAttempts(
'https://auth.example.com/tenant1/'
);

expect(attempts).toEqual([
{
kind: 'RFC8414',
variant: 'path-insert',
url: 'https://auth.example.com/.well-known/oauth-authorization-server/tenant1/'
},
{
kind: 'OIDC',
variant: 'path-insert',
url: 'https://auth.example.com/.well-known/openid-configuration/tenant1/'
},
{
kind: 'OIDC',
variant: 'path-append',
url: 'https://auth.example.com/tenant1/.well-known/openid-configuration'
}
]);
});
});
Loading