← Files Willy SEOARCHIVED FILE

dist/src/server.js

10.1 KB · Oct 2, 2026 · 00:34 UTC

↓ Download file

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 { Extractor } from './extractor.js';
import { Auditor } from './auditor.js';
import { estimateTitlePixelWidth } from './rules/onpage.js';
import { generateSchemaTemplate } from './rules/schema.js';
const server = new Server({
    name: 'willy-seo',
    version: '1.0.0',
}, {
    capabilities: {
        tools: {},
    },
});
// 1. Listar herramientas disponibles
server.setRequestHandler(ListToolsRequestSchema, async () => {
    return {
        tools: [
            {
                name: 'audit_url',
                description: 'Audita completamente una página web en vivo para SEO On-Page, técnico, semántica y datos estructurados bajo la metodología Willy SEO.',
                inputSchema: {
                    type: 'object',
                    properties: {
                        url: {
                            type: 'string',
                            description: 'URL completa de la página a auditar (ej: https://ejemplo.com/pagina)',
                        },
                        targetKeyword: {
                            type: 'string',
                            description: 'Palabra clave o intención de búsqueda objetivo (opcional)',
                        },
                    },
                    required: ['url'],
                },
            },
            {
                name: 'audit_content',
                description: 'Audita un borrador de contenido o código HTML antes de publicarlo, evaluando estructura de encabezados, longitud, metadatos y adecuación a la palabra clave.',
                inputSchema: {
                    type: 'object',
                    properties: {
                        htmlOrText: {
                            type: 'string',
                            description: 'El contenido HTML o texto del borrador a evaluar',
                        },
                        url: {
                            type: 'string',
                            description: 'URL prevista de publicación (opcional, ej: https://ejemplo.com/blog/mi-post)',
                        },
                        targetKeyword: {
                            type: 'string',
                            description: 'Palabra clave objetivo para optimizar',
                        },
                    },
                    required: ['htmlOrText'],
                },
            },
            {
                name: 'serp_preview',
                description: 'Calcula el ancho en píxeles y simula cómo se verá el snippet en los resultados de búsqueda de Google para móvil y escritorio.',
                inputSchema: {
                    type: 'object',
                    properties: {
                        title: {
                            type: 'string',
                            description: 'Texto del Title Tag',
                        },
                        description: {
                            type: 'string',
                            description: 'Texto de la Meta Description',
                        },
                        url: {
                            type: 'string',
                            description: 'URL a mostrar en el snippet',
                        },
                    },
                    required: ['title', 'description', 'url'],
                },
            },
            {
                name: 'generate_schema',
                description: 'Genera marcado estructurado Schema.org JSON-LD validado y listo para copiar/pegar.',
                inputSchema: {
                    type: 'object',
                    properties: {
                        schemaType: {
                            type: 'string',
                            enum: ['Article', 'FAQPage', 'Organization', 'Product', 'LocalBusiness'],
                            description: 'Tipo de schema a generar',
                        },
                        url: {
                            type: 'string',
                            description: 'URL de la página asociada',
                        },
                        title: {
                            type: 'string',
                            description: 'Título o nombre del elemento',
                        },
                        description: {
                            type: 'string',
                            description: 'Descripción breve',
                        },
                        faqs: {
                            type: 'array',
                            description: 'Lista de preguntas y respuestas (para tipo FAQPage)',
                            items: {
                                type: 'object',
                                properties: {
                                    question: { type: 'string' },
                                    answer: { type: 'string' },
                                },
                                required: ['question', 'answer'],
                            },
                        },
                    },
                    required: ['schemaType', 'url'],
                },
            },
        ],
    };
});
// 2. Ejecutar herramientas
server.setRequestHandler(CallToolRequestSchema, async (request) => {
    const { name, arguments: args } = request.params;
    try {
        switch (name) {
            case 'audit_url': {
                const url = String(args?.url);
                const targetKeyword = args?.targetKeyword ? String(args.targetKeyword) : undefined;
                const extractedData = await Extractor.extractFromUrl(url);
                const report = Auditor.runAudit(extractedData, targetKeyword);
                return {
                    content: [
                        {
                            type: 'text',
                            text: JSON.stringify(report, null, 2),
                        },
                    ],
                };
            }
            case 'audit_content': {
                const htmlOrText = String(args?.htmlOrText);
                const url = args?.url ? String(args.url) : 'https://ejemplo.com/draft';
                const targetKeyword = args?.targetKeyword ? String(args.targetKeyword) : undefined;
                // Si es texto plano sin tags HTML, envolverlo en párrafos básicos
                const isHtml = /<[a-z][\s\S]*>/i.test(htmlOrText);
                const formattedHtml = isHtml ? htmlOrText : `<html><body><p>${htmlOrText.replace(/\n\n/g, '</p><p>')}</p></body></html>`;
                const extractedData = Extractor.parseHtml(formattedHtml, url, 200, 50, {});
                const report = Auditor.runAudit(extractedData, targetKeyword);
                return {
                    content: [
                        {
                            type: 'text',
                            text: JSON.stringify(report, null, 2),
                        },
                    ],
                };
            }
            case 'serp_preview': {
                const title = String(args?.title || '');
                const description = String(args?.description || '');
                const url = String(args?.url || '');
                const pxWidth = estimateTitlePixelWidth(title);
                const desktopTruncated = pxWidth > 580;
                const mobileTruncated = pxWidth > 520;
                const result = {
                    desktop: {
                        title: desktopTruncated ? title.substring(0, 58) + '...' : title,
                        pixelWidth: pxWidth,
                        isTruncated: desktopTruncated,
                        url,
                        description: description.length > 158 ? description.substring(0, 155) + '...' : description,
                    },
                    mobile: {
                        title: mobileTruncated ? title.substring(0, 50) + '...' : title,
                        pixelWidth: pxWidth,
                        isTruncated: mobileTruncated,
                        url,
                        description: description.length > 120 ? description.substring(0, 118) + '...' : description,
                    },
                };
                return {
                    content: [
                        {
                            type: 'text',
                            text: JSON.stringify(result, null, 2),
                        },
                    ],
                };
            }
            case 'generate_schema': {
                const schemaType = args?.schemaType || 'Article';
                const url = String(args?.url || 'https://ejemplo.com');
                const title = args?.title ? String(args.title) : undefined;
                const description = args?.description ? String(args.description) : undefined;
                const faqs = args?.faqs || undefined;
                const jsonLd = generateSchemaTemplate(schemaType, {
                    url,
                    title,
                    description,
                    faqs,
                });
                return {
                    content: [
                        {
                            type: 'text',
                            text: jsonLd,
                        },
                    ],
                };
            }
            default:
                throw new Error(`Herramienta desconocida: ${name}`);
        }
    }
    catch (error) {
        return {
            isError: true,
            content: [
                {
                    type: 'text',
                    text: `Error ejecutando ${name}: ${error?.message || String(error)}`,
                },
            ],
        };
    }
});
// 3. Iniciar servidor sobre transporte stdio
async function main() {
    const transport = new StdioServerTransport();
    await server.connect(transport);
    console.error('🚀 Willy SEO MCP Server iniciado y listo para recibir peticiones de ChatGPT.');
}
main().catch((err) => {
    console.error('Fatal error en Willy SEO Server:', err);
    process.exit(1);
});

SHA-256: e20836e5935855726cac561ffe6bce1b4b16ccf24ea1ea22d56d7ce71c17096a