// src/server.ts
// MCP Server implementation for SEO Audit tools
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import {
analyzePage,
crawlSite,
runLighthouse,
analyzeSiteAccess,
checkLighthouseInstalled,
planAudit,
samplePages,
runAudit,
TOOL_DEFINITIONS,
} from './tools/index.js';
import { checkUrls } from './utils/http.js';
import { closeBrowser } from './utils/browser.js';
/**
* Create and configure the MCP server
*/
export function createServer(): Server {
const server = new Server(
{
name: 'seo-audit-mcp',
version: '0.1.0',
},
{
capabilities: {
tools: {},
},
}
);
// Register tool list handler
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: TOOL_DEFINITIONS,
};
});
// Register tool call handler
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args = {} } = request.params;
try {
switch (name) {
case 'analyze_page': {
const result = await analyzePage({
url: args.url as string,
waitForSelector: args.waitForSelector as string | undefined,
timeout: args.timeout as number | undefined,
device: args.device as 'desktop' | 'mobile' | undefined,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'crawl_site': {
const result = await crawlSite({
startUrl: args.startUrl as string,
maxPages: args.maxPages as number | undefined,
maxDepth: args.maxDepth as number | undefined,
includePatterns: args.includePatterns as string[] | undefined,
excludePatterns: args.excludePatterns as string[] | undefined,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'run_lighthouse': {
// Check if Lighthouse is installed
const isInstalled = await checkLighthouseInstalled();
if (!isInstalled) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: 'Lighthouse CLI is not installed',
solution: 'Run: npm install -g lighthouse',
}),
},
],
isError: true,
};
}
const result = await runLighthouse({
url: args.url as string,
device: args.device as 'mobile' | 'desktop' | undefined,
categories: args.categories as any[] | undefined,
saveReport: args.saveReport as boolean | undefined,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'analyze_sitemap': {
const result = await analyzeSiteAccess({
baseUrl: args.baseUrl as string,
includeSitemapUrls: args.includeSitemapUrls as boolean | undefined,
maxUrls: args.maxUrls as number | undefined,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'check_urls': {
const result = await checkUrls(
args.urls as string[],
{ timeout: args.timeout as number | undefined }
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'plan_audit': {
const result = await planAudit({
baseUrl: args.baseUrl as string,
maxSitemapsToProcess: args.maxSitemapsToProcess as number | undefined,
maxUrlsPerSitemap: args.maxUrlsPerSitemap as number | undefined,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'sample_pages': {
const result = await samplePages({
plan: args.plan as any,
routeTypes: args.routeTypes as string[] | undefined,
samplesOverride: args.samplesOverride as Record<string, number> | undefined,
concurrency: args.concurrency as number | undefined,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'run_audit': {
const result = await runAudit({
baseUrl: args.baseUrl as string,
reportsDir: args.reportsDir as string | undefined,
maxSitemaps: args.maxSitemaps as number | undefined,
maxUrlsPerSitemap: args.maxUrlsPerSitemap as number | undefined,
samplesPerRouteType: args.samplesPerRouteType as number | undefined,
concurrency: args.concurrency as number | undefined,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
default:
return {
content: [
{
type: 'text',
text: `Unknown tool: ${name}`,
},
],
isError: true,
};
}
} catch (error: any) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: error.message,
stack: error.stack,
}),
},
],
isError: true,
};
}
});
return server;
}
/**
* Run the server
*/
export async function runServer(): Promise<void> {
const server = createServer();
const transport = new StdioServerTransport();
// Handle cleanup on exit
process.on('SIGINT', async () => {
await closeBrowser();
process.exit(0);
});
process.on('SIGTERM', async () => {
await closeBrowser();
process.exit(0);
});
await server.connect(transport);
console.error('SEO Audit MCP Server running on stdio');
}