tsdown + cli with commander (#17)

* feat: tsdown

* fix: formatting

* automate version updates in cli

* fix: docs

* scenarios

* fix: commander deps
This commit is contained in:
Matt 2025-11-11 11:51:55 +00:00 • committed by GitHub
commit 7f1515c475
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

164
src/index.ts Normal file
View file

@ -0,0 +1,164 @@
#!/usr/bin/env node
import { Command } from 'commander';
import { ZodError } from 'zod';
import {
runConformanceTest,
printClientResults,
runServerConformanceTest,
printServerResults,
printServerSummary
} from './runner';
import { listScenarios, listClientScenarios } from './scenarios';
import { ConformanceCheck } from './types';
import { ClientOptionsSchema, ServerOptionsSchema } from './schemas';
import packageJson from '../package.json';
const program = new Command();
program
.name('conformance')
.description('MCP Conformance Test Suite')
.version(packageJson.version);
// Client command - tests a client implementation against scenarios
program
.command('client')
.description('Run conformance tests against a client implementation')
.requiredOption('--command <command>', 'Command to run the client')
.requiredOption('--scenario <scenario>', 'Scenario to test')
.option('--timeout <ms>', 'Timeout in milliseconds', '30000')
.option('--verbose', 'Show verbose output')
.action(async (options) => {
try {
// Validate options with Zod
const validated = ClientOptionsSchema.parse(options);
const result = await runConformanceTest(
validated.command,
validated.scenario,
validated.timeout
);
const { failed } = printClientResults(
result.checks,
validated.verbose ?? false
);
process.exit(failed > 0 ? 1 : 0);
} catch (error) {
if (error instanceof ZodError) {
console.error('Validation error:');
error.errors.forEach((err) => {
console.error(` ${err.path.join('.')}: ${err.message}`);
});
console.error('\nAvailable client scenarios:');
listScenarios().forEach((s) => console.error(` - ${s}`));
process.exit(1);
}
console.error('Client test error:', error);
process.exit(1);
}
});
// Server command - tests a server implementation
program
.command('server')
.description('Run conformance tests against a server implementation')
.requiredOption('--url <url>', 'URL of the server to test')
.option(
'--scenario <scenario>',
'Scenario to test (defaults to all scenarios if not specified)'
)
.action(async (options) => {
try {
// 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);
process.exit(failed > 0 ? 1 : 0);
} else {
// Run all scenarios
const scenarios = listClientScenarios();
console.log(
`Running ${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) {
console.error('Validation error:');
error.errors.forEach((err) => {
console.error(` ${err.path.join('.')}: ${err.message}`);
});
console.error('\nAvailable server scenarios:');
listClientScenarios().forEach((s) => console.error(` - ${s}`));
process.exit(1);
}
console.error('Server test error:', error);
process.exit(1);
}
});
// List scenarios command
program
.command('list')
.description('List available test scenarios')
.option('--client', 'List client scenarios')
.option('--server', 'List server scenarios')
.action((options) => {
if (options.server || (!options.client && !options.server)) {
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)) {
console.log('');
}
console.log('Client scenarios (test against a client):');
const clientScenarios = listScenarios();
clientScenarios.forEach((s) => console.log(` - ${s}`));
}
});
program.parse();