DEV Community

frank edekobi
frank edekobi

Posted on

Your coding assistant can now run a real SEO engine instead of guessing at one

Ask an AI assistant whether a page has SEO problems and you get an answer that reads well and cannot be checked. It might catch the missing canonical tag. It might invent a rule that does not exist. It might tell you the title is fine when there is no title. You have no way to tell which happened without knowing the answer already.

SEOCode now ships an MCP server, so the assistant stops answering from memory and starts calling the engine. Same rule engine that runs in our CLI and in our GitHub App PR review. It runs locally, on the file you point it at, and the model only formats what comes back.

Setup

One config block:

{
  "mcpServers": {
    "seocode": {
      "command": "npx",
      "args": ["-y", "-p", "@qobi/seocode", "seocode-mcp"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

That goes in Claude Desktop's claude_desktop_config.json, Cursor's MCP settings, or .mcp.json for Claude Code. Node 20 or newer. No account, no API key, no project setup.

The client launches seocode-mcp as a local child process and talks to it over stdio. Nothing is uploaded, and there is no network call in the audit path at all.

What it looks like

You ask your assistant to check a file. It calls audit_file and gets back something like this:

**app/products/[slug]/page.tsx** - 1 critical · 6 warning · 7 info

### 🔴 Critical
- **geo-ssr-hydration** - Critical metadata is set inside a useEffect hook, so it
  only exists after JavaScript runs on the client. AI crawlers process the
  server-rendered HTML first, so they never see it.
  - Fix: Move it into the Next.js Metadata API, export const metadata or
    export async function generateMetadata().

### 🟡 Warning
- **title-too-short**:6 - Title tags under 30 characters miss keyword opportunities.
  - Fix: Expand the title to at least 30 characters. Current: 24 chars.
- **missing-og-image** - Social shares with no og:image show no preview image.
  - Fix: Add <meta property="og:image" content="..." /> inside <head>.
    Recommended size 1200x630.
Enter fullscreen mode Exit fullscreen mode

Every finding carries the rule id, the line where it can be pinned, and the fix. Then you say "fix the lazy loading one" and the assistant calls suggest_fix:

**1-click fix for `image-missing-lazy-loading`** in `public/gallery.html` (line 11):

  <img loading="lazy" src="/big-photo.jpg" alt="A large decorative photo of the skyline" />

This is a deterministic replacement - apply it directly.
Enter fullscreen mode Exit fullscreen mode

That replacement is not written by the model. The engine produced it, then applied it against the current file to confirm it lands cleanly before handing it over.

The two tools

Tool Arguments Returns
audit_file filePath required, content optional for an unsaved buffer Findings grouped by severity, each with rule id, line, and fix
suggest_fix filePath, ruleId required, line optional A deterministic code replacement, or written guidance when the rule has no safe rewrite

suggest_fix will tell you when a rule has no auto-fix rather than producing one. Only transforms that can be made with certainty, like adding loading="lazy" or rel="noopener noreferrer", come back as replacements. Everything else comes back as guidance, labelled as guidance.

Why a tool call beats a prompt

The split matters more than it sounds. The engine does the SEO logic deterministically. The model is the interface: it decides which file you meant and reads the result back to you. The checks themselves cannot drift, cannot hallucinate a rule, and cannot be argued into a different answer on the second ask.

It is also the same engine, not a simplified copy. The MCP server loads the same rules/seo-rules.json the CLI and the GitHub App load, so a file that passes in your editor passes in the PR.

Two things it inherits from that engine:

Framework awareness. It reads titles and descriptions through Next.js generateMetadata(), Remix meta(), and Astro frontmatter the way the framework actually renders them. A page whose title comes from generateMetadata is not reported as missing a title. This is the part that makes the difference between a checker you keep and one you turn off after a day.

Your config. It walks up from the audited file looking for .seocode.json and honors the rule disables and severity overrides it finds there. The same file your team already committed for the CLI and the PR review.

What it does not do

It audits the file you point it at, when you ask. It does not scan your repo, track history, hold team config, or gate anything.

That is deliberate. One file on demand is the right scope for an editor. Enforcing the same rules on every pull request, for everyone on the team, without anyone remembering to ask, is a different job and that is what the GitHub App does.

How it is built

MCP is a smaller protocol than the tooling around it suggests. The whole server is about 260 lines with no SDK and no dependencies beyond what the engine already uses.

MCP over stdio is newline-delimited JSON-RPC 2.0. The client writes one JSON object per line to your process's stdin, you write one JSON object per line back to stdout. That is the entire transport. Four methods carry a tools-only server:

switch (method) {
  case 'initialize':
    return result(id, {
      protocolVersion: params?.protocolVersion || '2025-06-18',
      capabilities: { tools: {} },
      serverInfo: { name: 'seocode-mcp', version: version() },
    });
  case 'ping':
    return result(id, {});
  case 'tools/list':
    return result(id, { tools: TOOLS });
  case 'tools/call': {
    const text = await callTool(params.name, params.arguments ?? {});
    return result(id, { content: [{ type: 'text', text }] });
  }
  default:
    return error(id, -32601, `Method not found: ${method}`);
}
Enter fullscreen mode Exit fullscreen mode

Three details worth knowing if you write one of these:

  1. stdout belongs to the protocol. One stray console.log and the client sees a malformed message. All logging goes to stderr, including the startup banner.
  2. Notifications get no reply. A message with no id is a notification. Answering it is a protocol error.
  3. Buffer by newline, not by chunk. A single stdin chunk can hold two messages or half of one, so you accumulate and split on \n.

Because it is plain JSON-RPC on stdio, you can drive it without any client at all:

printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
  | npx -y -p @qobi/seocode seocode-mcp
Enter fullscreen mode Exit fullscreen mode

That is also the fastest way to debug a server the client says is failing to start.

Try it

The MCP server is free and needs no account. Setup and troubleshooting live at seocode.io/mcp.

If you want the same rules enforced on every pull request instead of only when someone asks, the GitHub App is free for public repositories: seocode.io.

And if you only want to know whether you are blocking AI crawlers right now, the robots.txt checker at seocode.io/crawlers answers in a second with no signup.

Top comments (1)

Collapse
 
citedy profile image
Dmitry Sergeev

We need to write a short YouTube comment as a regular developer, casual, referencing the video. Must not use marketing language. Should ask a question or specific reaction. The video is about a coding assistant now can run a real SEO engine instead of guessing. So comment could be like "does it actually fetch PageSpeed scores or just meta tags?" Keep it short, one or two sentences. Use lowercase start. No quotes around comment. No markdown. No URLs. No double hyphen. No curly quotes. Use straight ASCII.