MCP server exposing SEO.md/PRODUCTHUNT.md/LINKEDIN.md content-contract validation (hard-gate linting + JSON-LD generation) as tools any agent can call.
Four tools with complete input/output schemas and detailed descriptions. Tool names follow verb_noun pattern (lint_*, export_*). Descriptions are comprehensive (200-400 chars) and explain WHAT, WHEN, and RETURN. All parameters have types and descriptions. Output schemas are documented with structuredContent. Error handling is explicit (YAML parse errors, missing title/description reported as hard gates, not exceptions). Annotations present (readOnlyHint, idempotentHint). Main gaps: no enum constraints on config fields, no pagination/limits (not applicable here), and error recovery guidance could be more actionable for LLM self-correction.
Genera un JSON-LD válido (schema.org) a partir del bloque `schema:` de un SEO.md, sin LLM de por medio — templating puro sobre los campos declarados. Nunca ambigüedad, nunca alucinación. Args: - config (string): frontmatter YAML de un SEO.md. - url (string, opcional): URL canónica a incluir en el JSON-LD. Returns: el JSON-LD generado, más structuredContent con el objeto crudo.
Valida un copy de feed y/o un artículo de newsletter de LinkedIn contra los techos duros de un LINKEDIN.md. Los dos formatos son schemas separados (feed_copy / newsletter_article) porque casi no comparten restricciones reales — mandá al menos uno de los dos, podés mandar ambos. El contenido del artículo es estructurado (title, seoDescription, hook, contextParagraph, sections: [{heading, body}], closing), no texto libre a re-parsear con regex de headings — contar secciones es leer la longitud del array. forbidden_structure en LinkedIn SÍ chequea "table", "raw_html" y "literal_markdown" (a diferencia de lint_web_content, donde el medio ya es HTML y solo "table" aplica) — el editor de LinkedIn no renderiza ninguno de los tres. Args: - config (string): frontmatter YAML de tu LINKEDIN.md. - feedCopy (opcional): { text, hashtags? }. - newsletter (opcional): { title, seoDescription, hook?, contextParagraph?, sections: [{heading, body}], closing? }. Returns: texto "PASS"/"FAIL" con el detalle de cada pieza enviada, más structuredContent.
Valida un tagline + descripción de lanzamiento de Product Hunt contra los techos duros declarados en un PRODUCTHUNT.md. A diferencia de lint_web_content, los términos prohibidos (negative.terms) se bloquean en TODO el post, no solo en una superficie corta — no existe un uso legítimo de "votá por nosotros" en tu propio tagline o descripción (Product Hunt lo prohíbe sin matices y remueve el post). Args: - config (string): frontmatter YAML de tu PRODUCTHUNT.md. - tagline (string), description (string): el contenido a validar. - imageCount (number, opcional): cantidad de imágenes en la galería, para chequear contra gallery.min_images. Returns: texto "PASS"/"FAIL" con el detalle, más structuredContent.
Error messages lack actionable recovery guidance for LLM self-correction. When YAML parsing fails or required fields are missing, the tool returns a hard gate violation but does not suggest what the LLM should do next (e.g., 'Ensure config includes title, description, and locale fields').
Config parameter accepts free-form YAML string with no schema validation hints. LLMs cannot infer valid config structure from the description alone. Should document required top-level keys (e.g., 'Must include: title, description, locale, keywords, negative.terms, gallery.min_images') or provide an example structure.
Output structuredContent schema is documented but return type for text content is implicit. The tool returns {content: [{type: 'text', text: string}], structuredContent: object}, but the description does not explicitly state this dual-return structure. LLMs may not know to extract both fields.
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-23 | C | 63 | 2025-06-18+ | v2 |
Valida una o más páginas HTML contra los techos/pisos duros declarados en un SEO.md (protocolo: github.com/MauricioPerera/seo-md). No escribe contenido — solo verifica lo que ya se escribió. Chequea: <title>/<meta description> dentro de sus presupuestos de caracteres (max_chars, y min_chars si está declarado), <html lang> contra locale, que cada página no-pilar linkee a una pillar_page, títulos duplicados entre páginas, términos prohibidos bloqueados en <title>/<meta description> (informados, no bloqueados, si aparecen en el body — evita falsos positivos en frases comparativas), techo de densidad de keywords (nunca exige presencia), forbidden_structure (solo "table" tiene sentido en HTML), y validez del JSON-LD si está presente. Args: - config (string): el bloque YAML de frontmatter de tu SEO.md, tal cual (sin los `---`). - pages (array): páginas a validar, cada una { ruta, html }. Returns: texto "PASS"/"FAIL" seguido del detalle por página (✗ = gate duro que bloquea, · = señal blanda informativa que nunca bloquea), más structuredContent con el resultado crudo. Errores: si `config` no es YAML válido, o si a una página le falta <title>/<meta name="description">, se reporta como violación de gate duro, no como excepción — sigue devolviendo PASS/FAIL sobre el resto.
No guidance on when to use lint_web_content vs export_json_ld. Both accept a config parameter; the distinction (validation vs generation) is clear in descriptions but not in a composition note. LLMs may call both when only one is needed.
imageCount parameter in lint_producthunt_post is optional but no default is documented. If omitted, does the tool skip gallery validation or assume 0 images? Ambiguity forces LLM to guess or make extra calls.