Skip to content
Draft
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
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"start": "tsx src/index.ts",
"test": "vitest run",
"test:watch": "vitest",
"build": "tsdown src/index.ts --minify --clean --target node20",
"build": "tsdown src/index.ts src/fake-auth-server.ts --minify --clean --target node20",
"lint": "eslint src/ examples/ && prettier --check .",
"lint:fix": "eslint src/ examples/ --fix && prettier --write .",
"lint:fix_check": "npm run lint:fix && git diff --exit-code --quiet",
Expand All @@ -26,7 +26,8 @@
"dist"
],
"bin": {
"conformance": "dist/index.js"
"conformance": "dist/index.js",
"fake-auth-server": "dist/fake-auth-server.js"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
Expand Down
49 changes: 49 additions & 0 deletions src/fake-auth-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env node

import { Command } from 'commander';
import { createAuthServer } from './scenarios/client/auth/helpers/createAuthServer';
import { ServerLifecycle } from './scenarios/client/auth/helpers/serverLifecycle';
import type { ConformanceCheck } from './types';

function printServerInfo(url: string): void {
console.log(`Fake Auth Server running at ${url}`);
console.log('');
console.log('Endpoints:');
console.log(` Metadata: ${url}/.well-known/oauth-authorization-server`);
console.log(` Authorization: ${url}/authorize`);
console.log(` Token: ${url}/token`);
console.log(` Registration: ${url}/register`);
console.log(` Introspection: ${url}/introspect`);
console.log('');
console.log('Press Ctrl+C to stop');
}

const program = new Command();

program
.name('fake-auth-server')
.description(
'Standalone fake OAuth authorization server for testing MCP clients'
)
.option('--port <port>', 'Port to listen on (0 for random)', '0')
.action(async (options) => {
const port = parseInt(options.port, 10);
const checks: ConformanceCheck[] = [];
const lifecycle = new ServerLifecycle();

const app = createAuthServer(checks, lifecycle.getUrl, {
loggingEnabled: true
});

const url = await lifecycle.start(app, port !== 0 ? port : undefined);
printServerInfo(url);

// Handle graceful shutdown
process.on('SIGINT', async () => {
console.log('\nShutting down...');
await lifecycle.stop();
process.exit(0);
});
});

program.parse();
232 changes: 191 additions & 41 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
runConformanceTest,
printClientResults,
runServerConformanceTest,
runServerAuthConformanceTest,
startFakeAuthServer,
printServerResults,
printServerSummary,
runInteractiveMode
Expand All @@ -16,7 +18,8 @@ import {
listActiveClientScenarios,
listPendingClientScenarios,
listAuthScenarios,
listMetadataScenarios
listMetadataScenarios,
listServerAuthScenarios
} from './scenarios';
import { ConformanceCheck } from './types';
import { ClientOptionsSchema, ServerOptionsSchema } from './schemas';
Expand Down Expand Up @@ -199,69 +202,84 @@ program
program
.command('server')
.description('Run conformance tests against a server implementation')
.requiredOption('--url <url>', 'URL of the server to test')
.option(
'--url <url>',
'URL of the server to test (for already-running servers)'
)
.option(
'--command <cmd>',
'Command to start the server (for auth suite: spawns fake AS and passes MCP_CONFORMANCE_AUTH_SERVER_URL)'
)
.option(
'--scenario <scenario>',
'Scenario to test (defaults to active suite if not specified)'
)
.option(
'--suite <suite>',
'Suite to run: "active" (default, excludes pending), "all", or "pending"',
'Suite to run: "active" (default, excludes pending), "all", "pending", or "auth"',
'active'
)
.option('--timeout <ms>', 'Timeout in milliseconds', '30000')
.option('--verbose', 'Show verbose output (JSON instead of pretty print)')
.option(
'--interactive',
'Interactive auth mode: opens browser for login instead of auto-redirect'
)
.action(async (options) => {
try {
// Validate options with Zod
const validated = ServerOptionsSchema.parse(options);

const verbose = options.verbose ?? false;
const timeout = parseInt(options.timeout, 10);
const suite = options.suite?.toLowerCase() || 'active';

// If a single scenario is specified, run just that one
if (validated.scenario) {
const result = await runServerConformanceTest(
validated.url,
validated.scenario
);
// Check if this is an auth test
const isAuthTest =
suite === 'auth' || options.scenario?.startsWith('server-auth/');

const { failed } = printServerResults(
result.checks,
result.scenarioDescription,
verbose
);
process.exit(failed > 0 ? 1 : 0);
} else {
// Run scenarios based on suite
const suite = options.suite?.toLowerCase() || 'active';
let scenarios: string[];
if (isAuthTest) {
// Auth testing mode - requires --url or --command
if (!options.url && !options.command) {
console.error(
'For auth testing, either --url or --command is required'
);
console.error('\n--url <url> URL of already running server');
console.error(
'--command <cmd> Command to start the server (conformance spawns fake AS)'
);
process.exit(1);
}

if (suite === 'all') {
scenarios = listClientScenarios();
} else if (suite === 'active') {
scenarios = listActiveClientScenarios();
} else if (suite === 'pending') {
scenarios = listPendingClientScenarios();
// Get scenarios to run
let scenarios: string[];
if (options.scenario) {
scenarios = [options.scenario];
} else {
console.error(`Unknown suite: ${suite}`);
console.error('Available suites: active, all, pending');
process.exit(1);
scenarios = listServerAuthScenarios();
}

console.log(
`Running ${suite} suite (${scenarios.length} scenarios) against ${validated.url}\n`
);
console.log(`Running auth suite (${scenarios.length} scenarios)...\n`);

const allResults: { scenario: string; checks: ConformanceCheck[] }[] =
[];

for (const scenarioName of scenarios) {
console.log(`\n=== Running scenario: ${scenarioName} ===`);
try {
const result = await runServerConformanceTest(
validated.url,
scenarioName
);
const result = await runServerAuthConformanceTest({
url: options.url,
command: options.command,
scenarioName,
timeout,
interactive: options.interactive
});
allResults.push({ scenario: scenarioName, checks: result.checks });

if (verbose) {
printServerResults(
result.checks,
result.scenarioDescription,
verbose
);
}
} catch (error) {
console.error(`Failed to run scenario ${scenarioName}:`, error);
allResults.push({
Expand All @@ -283,6 +301,85 @@ program

const { totalFailed } = printServerSummary(allResults);
process.exit(totalFailed > 0 ? 1 : 0);
} else {
// Standard server testing mode - requires --url
if (!options.url) {
console.error('--url is required for non-auth server testing');
process.exit(1);
}

// Validate options with Zod
const validated = ServerOptionsSchema.parse(options);

// If a single scenario is specified, run just that one
if (validated.scenario) {
const result = await runServerConformanceTest(
validated.url,
validated.scenario
);

const { failed } = printServerResults(
result.checks,
result.scenarioDescription,
verbose
);
process.exit(failed > 0 ? 1 : 0);
} else {
// Run scenarios based on suite
let scenarios: string[];

if (suite === 'all') {
scenarios = listClientScenarios();
} else if (suite === 'active') {
scenarios = listActiveClientScenarios();
} else if (suite === 'pending') {
scenarios = listPendingClientScenarios();
} else {
console.error(`Unknown suite: ${suite}`);
console.error('Available suites: active, all, pending, auth');
process.exit(1);
}

console.log(
`Running ${suite} suite (${scenarios.length} scenarios) against ${validated.url}\n`
);

const allResults: { scenario: string; checks: ConformanceCheck[] }[] =
[];

for (const scenarioName of scenarios) {
console.log(`\n=== Running scenario: ${scenarioName} ===`);
try {
const result = await runServerConformanceTest(
validated.url,
scenarioName
);
allResults.push({
scenario: scenarioName,
checks: result.checks
});
} catch (error) {
console.error(`Failed to run scenario ${scenarioName}:`, error);
allResults.push({
scenario: scenarioName,
checks: [
{
id: scenarioName,
name: scenarioName,
description: 'Failed to run scenario',
status: 'FAILURE',
timestamp: new Date().toISOString(),
errorMessage:
error instanceof Error ? error.message : String(error)
}
]
});
}
}

const { totalFailed } = printServerSummary(allResults);
process.exit(totalFailed > 0 ? 1 : 0);
}
}
} catch (error) {
if (error instanceof ZodError) {
Expand All @@ -292,6 +389,8 @@ program
});
console.error('\nAvailable server scenarios:');
listClientScenarios().forEach((s) => console.error(` - ${s}`));
console.error('\nAvailable server auth scenarios:');
listServerAuthScenarios().forEach((s) => console.error(` - ${s}`));
process.exit(1);
}
console.error('Server test error:', error);
Expand All @@ -305,15 +404,27 @@ program
.description('List available test scenarios')
.option('--client', 'List client scenarios')
.option('--server', 'List server scenarios')
.option('--server-auth', 'List server auth scenarios')
.action((options) => {
if (options.server || (!options.client && !options.server)) {
const showAll = !options.client && !options.server && !options.serverAuth;

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.serverAuth || showAll) {
if (options.server || showAll) {
console.log('');
}
console.log('Server auth scenarios (test server auth implementation):');
const authScenarios = listServerAuthScenarios();
authScenarios.forEach((s) => console.log(` - ${s}`));
}

if (options.client || showAll) {
if (options.server || options.serverAuth || showAll) {
console.log('');
}
console.log('Client scenarios (test against a client):');
Expand All @@ -322,4 +433,43 @@ program
}
});

// Fake auth server command - starts a standalone fake authorization server
program
.command('fake-auth-server')
.description(
'Start a standalone fake authorization server for manual testing'
)
.option('--port <port>', 'Port to listen on (default: random)')
.action(async (options) => {
const port = options.port ? parseInt(options.port, 10) : undefined;

console.log('Starting fake authorization server...');
const { url, stop } = await startFakeAuthServer(port);
console.log(`\nFake authorization server running at: ${url}`);
console.log('\nEndpoints:');
console.log(
` Metadata: ${url}/.well-known/oauth-authorization-server`
);
console.log(` Authorization: ${url}/authorize`);
console.log(` Token: ${url}/token`);
console.log(` Registration: ${url}/register`);
console.log('\nPress Ctrl+C to stop.');

// Handle graceful shutdown
process.on('SIGINT', async () => {
console.log('\nShutting down...');
await stop();
process.exit(0);
});

process.on('SIGTERM', async () => {
console.log('\nShutting down...');
await stop();
process.exit(0);
});

// Keep the process running
await new Promise(() => {});
});

program.parse();
2 changes: 2 additions & 0 deletions src/runner/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export {
// Export server functions
export {
runServerConformanceTest,
runServerAuthConformanceTest,
startFakeAuthServer,
printServerResults,
printServerSummary
} from './server';
Expand Down
Loading
Loading