index.tsā¢16.9 kB
#!/usr/bin/env node
/**
* Webby MCP Server - Comprehensive Website Validator
* Provides performance, accessibility, SEO, and security testing tools
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
// Import all tools
import { analyzePageSpeed } from './src/performance/pagespeed.js';
import { analyzeGTmetrix } from './src/performance/gtmetrix.js';
import { analyzeWebPageTest } from './src/performance/webpagetest.js';
import { analyzeWAVE } from './src/accessibility/wave.js';
import { analyzeAxe } from './src/accessibility/axe.js';
import { analyzeMozillaObservatory } from './src/security/mozilla-observatory.js';
import { analyzeSSLLabs, analyzeSSLLabsComplete } from './src/security/ssl-labs.js';
import {
runAllPerformance,
runAllAccessibility,
runAllSEO,
runAllSecurity,
runComprehensive,
} from './src/orchestrator/run-all.js';
// Zod schemas for validation
const PageSpeedArgsSchema = z.object({
url: z.string().url(),
strategy: z.enum(['mobile', 'desktop']).optional(),
apiKey: z.string().optional(),
});
const GTmetrixArgsSchema = z.object({
url: z.string().url(),
apiKey: z.string(),
location: z.string().optional(),
browser: z.string().optional(),
});
const WebPageTestArgsSchema = z.object({
url: z.string().url(),
location: z.string().optional(),
runs: z.number().optional(),
waitForResults: z.boolean().optional(),
timeout: z.number().optional(),
});
const WAVEArgsSchema = z.object({
url: z.string().url(),
apiKey: z.string(),
reporttype: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4)]).optional(),
});
const AxeArgsSchema = z.object({
url: z.string().url(),
wcagLevel: z.string().optional(),
});
const MozillaObservatoryArgsSchema = z.object({
url: z.string().url(),
forceRescan: z.boolean().optional(),
});
const SSLLabsArgsSchema = z.object({
url: z.string().url(),
email: z.string().email(),
maxAge: z.number().optional(),
startNew: z.boolean().optional(),
waitForComplete: z.boolean().optional(),
maxWaitMinutes: z.number().optional(),
});
const AllPerformanceArgsSchema = z.object({
url: z.string().url(),
pagespeedApiKey: z.string().optional(),
gtmetrixApiKey: z.string().optional(),
webpagetestEnabled: z.boolean().optional(),
webpagetestWaitForResults: z.boolean().optional(),
});
const AllAccessibilityArgsSchema = z.object({
url: z.string().url(),
waveApiKey: z.string().optional(),
wcagLevel: z.string().optional(),
});
const AllSEOArgsSchema = z.object({
url: z.string().url(),
});
const AllSecurityArgsSchema = z.object({
url: z.string().url(),
email: z.string().email(),
waitForSSL: z.boolean().optional(),
});
const ComprehensiveArgsSchema = z.object({
url: z.string().url(),
email: z.string().email(),
categories: z.array(z.enum(['performance', 'accessibility', 'seo', 'security'])).optional(),
pagespeedApiKey: z.string().optional(),
gtmetrixApiKey: z.string().optional(),
waveApiKey: z.string().optional(),
wcagLevel: z.string().optional(),
waitForSSL: z.boolean().optional(),
});
// Create server instance
const server = new Server(
{
name: 'webby',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// Define available tools
const tools: Tool[] = [
// Performance Tools
{
name: 'validate_performance_pagespeed',
description: 'Analyze website performance using Google PageSpeed Insights. Returns Core Web Vitals and performance scores. Free API with 25K requests/day.',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string', description: 'The URL to analyze' },
strategy: { type: 'string', enum: ['mobile', 'desktop'], description: 'Device type (default: mobile)' },
apiKey: { type: 'string', description: 'Optional API key for higher quota' },
},
required: ['url'],
},
},
{
name: 'validate_performance_gtmetrix',
description: 'Analyze website performance using GTmetrix. Requires API key (free tier available).',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string' },
apiKey: { type: 'string', description: 'GTmetrix API key (required)' },
location: { type: 'string', description: 'Test location (e.g., vancouver-canada)' },
browser: { type: 'string', description: 'Browser type (e.g., chrome)' },
},
required: ['url', 'apiKey'],
},
},
{
name: 'validate_performance_webpagetest',
description: 'Analyze website performance using WebPageTest via browser automation. Free 300 tests/month. Returns test ID immediately or waits for full results.',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string', description: 'The URL to analyze' },
location: { type: 'string', description: 'Test location (e.g., Dulles:Chrome)' },
runs: { type: 'number', description: 'Number of test runs (default: 1)' },
waitForResults: { type: 'boolean', description: 'Wait for test to complete (default: false, returns test ID immediately)' },
timeout: { type: 'number', description: 'Timeout in milliseconds (default: 300000 = 5 minutes)' },
},
required: ['url'],
},
},
// Accessibility Tools
{
name: 'validate_accessibility_wave',
description: 'Analyze website accessibility using WAVE. Tests WCAG compliance, errors, and contrast issues. Requires API key.',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string' },
apiKey: { type: 'string', description: 'WAVE API key (required)' },
reporttype: { type: 'number', enum: [1, 2, 3, 4], description: 'Detail level (1-4)' },
},
required: ['url', 'apiKey'],
},
},
{
name: 'validate_accessibility_axe',
description: 'Analyze website accessibility using Axe. Free, open-source, finds ~57% of WCAG issues with zero false positives.',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string' },
wcagLevel: { type: 'string', description: 'WCAG level (wcag2a, wcag2aa, wcag2aaa, wcag21aa, wcag22aa)' },
},
required: ['url'],
},
},
// Security Tools
{
name: 'validate_security_mozilla_observatory',
description: 'Analyze HTTP security headers using Mozilla Observatory. Tests CSP, HSTS, etc. Free API, 1 scan per minute per domain.',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string' },
forceRescan: { type: 'boolean', description: 'Force new scan (default: false)' },
},
required: ['url'],
},
},
{
name: 'validate_security_ssl_labs',
description: 'Analyze SSL/TLS configuration using SSL Labs. Comprehensive certificate and protocol analysis. Long-running (may take minutes).',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string' },
email: { type: 'string', description: 'Your email (required by API)' },
maxAge: { type: 'number', description: 'Max cached report age in hours' },
startNew: { type: 'boolean', description: 'Force new assessment' },
waitForComplete: { type: 'boolean', description: 'Wait for completion (default: false)' },
maxWaitMinutes: { type: 'number', description: 'Max wait time in minutes (default: 5)' },
},
required: ['url', 'email'],
},
},
// Category Runners
{
name: 'validate_all_performance',
description: 'Run all available performance tests (PageSpeed Insights + optionally WebPageTest + optionally GTmetrix).',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string' },
pagespeedApiKey: { type: 'string', description: 'Optional PageSpeed API key' },
gtmetrixApiKey: { type: 'string', description: 'Optional GTmetrix API key' },
webpagetestEnabled: { type: 'boolean', description: 'Run WebPageTest via browser automation (default: false)' },
webpagetestWaitForResults: { type: 'boolean', description: 'Wait for WebPageTest to complete (default: false)' },
},
required: ['url'],
},
},
{
name: 'validate_all_accessibility',
description: 'Run all accessibility tests (Axe + optionally WAVE if API key provided).',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string' },
waveApiKey: { type: 'string', description: 'Optional WAVE API key' },
wcagLevel: { type: 'string', description: 'WCAG level for Axe' },
},
required: ['url'],
},
},
{
name: 'validate_all_seo',
description: 'Run SEO analysis using PageSpeed Insights (includes Lighthouse SEO).',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string' },
},
required: ['url'],
},
},
{
name: 'validate_all_security',
description: 'Run all security tests (Mozilla Observatory + SSL Labs).',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string' },
email: { type: 'string', description: 'Email for SSL Labs' },
waitForSSL: { type: 'boolean', description: 'Wait for SSL Labs to complete' },
},
required: ['url', 'email'],
},
},
// Comprehensive Runner
{
name: 'validate_comprehensive',
description: 'Run comprehensive validation across all categories (performance, accessibility, SEO, security).',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string' },
email: { type: 'string', description: 'Email for SSL Labs (required)' },
categories: {
type: 'array',
items: { type: 'string', enum: ['performance', 'accessibility', 'seo', 'security'] },
description: 'Categories to test (default: all)'
},
pagespeedApiKey: { type: 'string' },
gtmetrixApiKey: { type: 'string' },
waveApiKey: { type: 'string' },
wcagLevel: { type: 'string' },
waitForSSL: { type: 'boolean' },
},
required: ['url', 'email'],
},
},
];
// List tools handler
server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools };
});
// Call tool handler
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
// Performance Tools
case 'validate_performance_pagespeed': {
const validatedArgs = PageSpeedArgsSchema.parse(args);
const result = await analyzePageSpeed(validatedArgs.url, {
strategy: validatedArgs.strategy,
apiKey: validatedArgs.apiKey,
});
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
}
case 'validate_performance_gtmetrix': {
const validatedArgs = GTmetrixArgsSchema.parse(args);
const result = await analyzeGTmetrix(validatedArgs.url, {
apiKey: validatedArgs.apiKey,
location: validatedArgs.location,
browser: validatedArgs.browser,
});
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
}
case 'validate_performance_webpagetest': {
const validatedArgs = WebPageTestArgsSchema.parse(args);
const result = await analyzeWebPageTest(validatedArgs.url, {
location: validatedArgs.location,
runs: validatedArgs.runs,
waitForResults: validatedArgs.waitForResults,
timeout: validatedArgs.timeout,
});
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
}
// Accessibility Tools
case 'validate_accessibility_wave': {
const validatedArgs = WAVEArgsSchema.parse(args);
const result = await analyzeWAVE(validatedArgs.url, {
apiKey: validatedArgs.apiKey,
reporttype: validatedArgs.reporttype,
});
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
}
case 'validate_accessibility_axe': {
const validatedArgs = AxeArgsSchema.parse(args);
const result = await analyzeAxe(validatedArgs.url, {
wcagLevel: validatedArgs.wcagLevel,
});
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
}
// Security Tools
case 'validate_security_mozilla_observatory': {
const validatedArgs = MozillaObservatoryArgsSchema.parse(args);
const result = await analyzeMozillaObservatory(validatedArgs.url, {
forceRescan: validatedArgs.forceRescan,
});
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
}
case 'validate_security_ssl_labs': {
const validatedArgs = SSLLabsArgsSchema.parse(args);
const result = validatedArgs.waitForComplete
? await analyzeSSLLabsComplete(
validatedArgs.url,
{ email: validatedArgs.email, maxAge: validatedArgs.maxAge, startNew: validatedArgs.startNew },
validatedArgs.maxWaitMinutes
)
: await analyzeSSLLabs(validatedArgs.url, {
email: validatedArgs.email,
maxAge: validatedArgs.maxAge,
startNew: validatedArgs.startNew,
});
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
}
// Category Runners
case 'validate_all_performance': {
const validatedArgs = AllPerformanceArgsSchema.parse(args);
const result = await runAllPerformance(validatedArgs.url, {
pagespeed: { apiKey: validatedArgs.pagespeedApiKey },
webpagetest: validatedArgs.webpagetestEnabled ? {
waitForResults: validatedArgs.webpagetestWaitForResults
} : undefined,
gtmetrix: validatedArgs.gtmetrixApiKey ? { apiKey: validatedArgs.gtmetrixApiKey } : undefined,
});
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
}
case 'validate_all_accessibility': {
const validatedArgs = AllAccessibilityArgsSchema.parse(args);
const result = await runAllAccessibility(validatedArgs.url, {
wave: validatedArgs.waveApiKey ? { apiKey: validatedArgs.waveApiKey } : undefined,
axe: { wcagLevel: validatedArgs.wcagLevel },
});
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
}
case 'validate_all_seo': {
const validatedArgs = AllSEOArgsSchema.parse(args);
const result = await runAllSEO(validatedArgs.url);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
}
case 'validate_all_security': {
const validatedArgs = AllSecurityArgsSchema.parse(args);
const result = await runAllSecurity(validatedArgs.url, {
sslLabs: {
email: validatedArgs.email,
waitForComplete: validatedArgs.waitForSSL,
},
});
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
}
// Comprehensive Runner
case 'validate_comprehensive': {
const validatedArgs = ComprehensiveArgsSchema.parse(args);
const result = await runComprehensive(validatedArgs.url, {
performance: {
pagespeed: { apiKey: validatedArgs.pagespeedApiKey },
gtmetrix: validatedArgs.gtmetrixApiKey ? { apiKey: validatedArgs.gtmetrixApiKey } : undefined,
},
accessibility: {
wave: validatedArgs.waveApiKey ? { apiKey: validatedArgs.waveApiKey } : undefined,
axe: { wcagLevel: validatedArgs.wcagLevel },
},
security: {
sslLabs: {
email: validatedArgs.email,
waitForComplete: validatedArgs.waitForSSL,
},
},
categories: validatedArgs.categories,
});
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid arguments: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`);
}
throw error;
}
});
// Start server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Webby MCP Server running on stdio');
}
main().catch((error) => {
console.error('Fatal error in main():', error);
process.exit(1);
});