import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
CallToolResult,
ListToolsRequestSchema,
Tool,
} from '@modelcontextprotocol/sdk/types.js';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { AccessibilityTester } from '../accessibility/tester.js';
import { FileOutputManager } from '../utils/fileOutput.js';
import { AccessibilityTestResult, MCPTestRequest, MCPTestResponse } from '../types/index.js';
export class AccessibilityMCPServer {
private server: Server;
private tester: AccessibilityTester;
private fileManager: FileOutputManager;
constructor() {
this.server = new Server(
{
name: 'accessibility-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.tester = new AccessibilityTester();
this.fileManager = new FileOutputManager();
this.setupToolHandlers();
}
private setupToolHandlers(): void {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'test_accessibility',
description: 'Run accessibility tests on a website using Playwright and axe-core against WCAG standards',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'The URL of the website to test for accessibility',
format: 'uri'
},
wcagLevel: {
type: 'string',
enum: ['A', 'AA', 'AAA'],
description: 'WCAG compliance level to test against',
default: 'AA'
},
wcagVersion: {
type: 'string',
enum: ['2.1', '2.2'],
description: 'WCAG version to test against',
default: '2.1'
},
browser: {
type: 'string',
enum: ['chromium', 'firefox', 'webkit'],
description: 'Browser engine to use for testing',
default: 'chromium'
},
includeScreenshot: {
type: 'boolean',
description: 'Whether to capture a screenshot of the page',
default: false
}
},
required: ['url']
}
},
{
name: 'get_test_results',
description: 'Retrieve saved accessibility test results by filename',
inputSchema: {
type: 'object',
properties: {
fileName: {
type: 'string',
description: 'Name of the test result file to retrieve'
}
},
required: ['fileName']
}
},
{
name: 'list_test_results',
description: 'List all available accessibility test result files',
inputSchema: {
type: 'object',
properties: {},
additionalProperties: false
}
}
] as Tool[]
};
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'test_accessibility':
return await this.handleAccessibilityTest(args as unknown as MCPTestRequest);
case 'get_test_results':
return await this.handleGetTestResults(args as unknown as { fileName: string });
case 'list_test_results':
return await this.handleListTestResults();
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error: ${error instanceof Error ? error.message : 'Unknown error occurred'}`
}
]
};
}
});
}
private async handleAccessibilityTest(request: MCPTestRequest): Promise<CallToolResult> {
console.log(`Starting accessibility test for: ${request.url}`);
// Validate URL
try {
new URL(request.url);
} catch {
throw new Error('Invalid URL provided');
}
// Initialize browser
await this.tester.initialize();
try {
// Run the test
const results = await this.tester.run(request.url, request.wcagLevel, request.criticality);
const testResult: AccessibilityTestResult = {
id: uuidv4(),
timestamp: new Date(),
url: request.url,
options: {
url: request.url,
wcagLevel: request.wcagLevel,
criticality: request.criticality,
browser: request.browser || 'chromium',
},
axeResults: results,
summary: {
violations: results.violations.length,
passes: results.passes.length,
incomplete: results.incomplete.length,
inapplicable: results.inapplicable.length,
},
};
// Save results to file
const filePath = await this.fileManager.saveTestResult(testResult);
// Also save raw JSON results for potential programmatic access
const rawFilePath = await this.fileManager.saveRawResults(testResult);
const response: MCPTestResponse = {
success: testResult.error === undefined,
testId: testResult.id,
filePath,
summary: {
violations: testResult.summary.violations,
passes: testResult.summary.passes,
url: testResult.url,
timestamp: testResult.timestamp.toISOString()
},
error: testResult.error
};
const summary = testResult.error
? `❌ Test failed: ${testResult.error}`
: `✅ Test completed successfully!\n\n` +
`📊 **Summary for ${testResult.url}:**\n` +
`- Violations: ${testResult.summary.violations}\n` +
`- Passes: ${testResult.summary.passes}\n` +
`- Incomplete: ${testResult.summary.incomplete}\n` +
`- Inapplicable: ${testResult.summary.inapplicable}\n\n` +
`📁 Results saved to: ${path.basename(filePath)}\n` +
`📄 Raw data: ${path.basename(rawFilePath)}`;
return {
content: [
{
type: 'text',
text: summary
}
]
};
} finally {
// Clean up browser
await this.tester.cleanup();
}
}
private async handleGetTestResults(args: { fileName: string }): Promise<CallToolResult> {
const content = await this.fileManager.getTestResult(args.fileName);
if (!content) {
throw new Error(`Test result file '${args.fileName}' not found`);
}
return {
content: [
{
type: 'text',
text: content
}
]
};
}
private async handleListTestResults(): Promise<CallToolResult> {
const files = await this.fileManager.listTestResults();
if (files.length === 0) {
return {
content: [
{
type: 'text',
text: 'No accessibility test results found.'
}
]
};
}
const fileList = files.map((file, index) => `${index + 1}. ${file}`).join('\n');
return {
content: [
{
type: 'text',
text: `Available test result files:\n\n${fileList}`
}
]
};
}
async start(): Promise<void> {
console.log('Starting Accessibility MCP Server...');
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.log('Accessibility MCP Server is running and ready to receive requests.');
}
async stop(): Promise<void> {
await this.tester.cleanup();
console.log('Accessibility MCP Server stopped.');
}
}
// Handle graceful shutdown
process.on('SIGINT', async () => {
console.log('\nReceived SIGINT, shutting down gracefully...');
process.exit(0);
});
process.on('SIGTERM', async () => {
console.log('\nReceived SIGTERM, shutting down gracefully...');
process.exit(0);
});