Skip to content

feat: MCP server — let agents audit sites and generate AEO files#66

Merged
rubenmarcus merged 7 commits into
feat/remote-url-checkfrom
feat/mcp-server
Jun 16, 2026
Merged

feat: MCP server — let agents audit sites and generate AEO files#66
rubenmarcus merged 7 commits into
feat/remote-url-checkfrom
feat/mcp-server

Conversation

@rubenmarcus

Copy link
Copy Markdown
Member

Stacked on #63 (uses the remote scan module) — merge that first; this PR's diff is against feat/remote-url-check.

What

npx aeo.js mcp runs a stdio MCP server exposing three tools:

  • audit_url — live GEO readiness scan of any site (score, crawler access matrix, citability, top fixes). "Audit example.com and tell me why ChatGPT can't see it."
  • score_citability — score draft content for AI citability with improvement hints; agents can iterate on a page until it's citable.
  • generate_aeo_files — generate the full AEO file set in the current project.

Setup is zero-install: claude mcp add aeo -- npx -y aeo.js mcp.

How

  • Minimal newline-delimited JSON-RPC over stdio (initialize, ping, tools/list, tools/call, notifications ignored) — no SDK dependency added, keeping the package lean.
  • Logs to stderr so the protocol stream stays clean.
  • Also fixes a pre-existing bug surfaced by serverInfo: the VERSION constant said 0.0.13 while package.json is 0.0.14 (--version was wrong too).
  • Docs: features/mcp page + sidebar entry.

Verification

  • tsc --noEmit clean; 220 tests pass (10 new: initialize/ping/tools-list, citability + audit tool calls with mocked fetch, error paths, notification handling)
  • Stdio smoke test against the built CLI: initialize and tools/list round-trip correctly

🤖 Generated with Claude Code

rubenmarcus and others added 3 commits June 10, 2026 13:18
Stdio MCP server exposing three tools to AI agents: audit_url (live
GEO readiness scan), score_citability (score draft content for AI
citability), and generate_aeo_files (generate robots.txt/llms.txt/...
in the current project). Implemented as minimal JSON-RPC over
newline-delimited stdio — initialize, ping, tools/list, tools/call —
so no SDK dependency is needed. Logs go to stderr to keep the
protocol stream clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The CLI --version output and MCP serverInfo were reporting 0.0.13.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
aeo-js Ready Ready Preview, Comment Jun 16, 2026 10:35pm

Request Review

@github-actions

Copy link
Copy Markdown

Docs Preview

Preview URL: https://feat-mcp-server.aeojs.pages.dev

This preview was deployed from the latest commit on this PR.

@greptile-apps

greptile-apps Bot commented Jun 10, 2026

Copy link
Copy Markdown

Greptile Summary

Adds a zero-dependency MCP server (npx aeo.js mcp) that exposes three tools over stdio JSON-RPC — audit_url, score_citability, and generate_aeo_files — and fixes the stale VERSION constant. The PR also adds documentation and 10 new tests covering the core request-handling paths.

  • src/core/mcp-server.ts: New file implementing the full stdio JSON-RPC loop, URL normalization, outDir path sandboxing, and all three tool implementations; previous review concerns (unhandled rejections, interface vs type, initialize echoing client version, generate_aeo_files lacking URL validation, and unrestricted outDir) are all addressed in this revision.
  • src/index.ts: Bumps VERSION from 0.0.13 to 0.0.14 to match package.json, fixing the --version output regression.

Confidence Score: 4/5

Safe to merge after fixing the missing stdin error handler — without it the long-running server process can be killed by an unexpected pipe error from the MCP client.

The stdin loop only registers data and end handlers; if the parent MCP client exits abnormally the OS may deliver an error event on stdin, which with no listener throws and exits the process. All other previous concerns were addressed in this revision.

src/core/mcp-server.ts — stdin error handler and Windows path separator in the outDir guard.

Important Files Changed

Filename Overview
src/core/mcp-server.ts New MCP server implementation — minimal JSON-RPC over stdio with three tools; stdin error handler missing (crashes on unexpected pipe closure), and outDir path check uses forward slash separator which fails on Windows.
src/core/mcp-server.test.ts New test suite with 10 tests covering initialize, ping, tools/list, citability scoring, mocked fetch for audit, error paths, and notification handling; good coverage of the public handleMcpRequest API.
src/cli.ts Adds mcp case to the CLI switch that lazy-imports and calls runMcpStdio(); straightforward and correct.
src/index.ts VERSION bumped from 0.0.13 to 0.0.14 to match package.json; fixes a pre-existing discrepancy.
website/astro.config.mjs Adds MCP Server sidebar entry to the documentation site; no issues.
website/src/content/docs/features/mcp.mdx New docs page covering setup, all three tools, and notes about network/Node requirements; accurate and complete.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client as MCP Client
    participant Server as runMcpStdio (stdio)
    participant Handler as handleMcpRequest
    participant Tools as callTool

    Client->>Server: "initialize {protocolVersion}"
    Server->>Handler: handleMcpRequest
    Handler-->>Server: "{protocolVersion: 2025-03-26, capabilities, serverInfo}"
    Server-->>Client: JSON-RPC response

    Client->>Server: notifications/initialized (no id)
    Server->>Handler: handleMcpRequest
    Handler-->>Server: null (notification, no response)

    Client->>Server: tools/list
    Server->>Handler: handleMcpRequest
    Handler-->>Server: "{tools: [audit_url, score_citability, generate_aeo_files]}"
    Server-->>Client: JSON-RPC response

    Client->>Server: "tools/call {name: audit_url, arguments: {url}}"
    Server->>Handler: handleMcpRequest
    Handler->>Tools: callTool(audit_url, args)
    Tools->>Tools: normalizeUrl(url)
    Tools->>Tools: discover(target) + crawlPages()
    Tools->>Tools: buildRemoteReport() / formatRemoteReport()
    Tools-->>Handler: textResult(formattedReport)
    Handler-->>Server: "{result: {content, isError}}"
    Server-->>Client: JSON-RPC response
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client as MCP Client
    participant Server as runMcpStdio (stdio)
    participant Handler as handleMcpRequest
    participant Tools as callTool

    Client->>Server: "initialize {protocolVersion}"
    Server->>Handler: handleMcpRequest
    Handler-->>Server: "{protocolVersion: 2025-03-26, capabilities, serverInfo}"
    Server-->>Client: JSON-RPC response

    Client->>Server: notifications/initialized (no id)
    Server->>Handler: handleMcpRequest
    Handler-->>Server: null (notification, no response)

    Client->>Server: tools/list
    Server->>Handler: handleMcpRequest
    Handler-->>Server: "{tools: [audit_url, score_citability, generate_aeo_files]}"
    Server-->>Client: JSON-RPC response

    Client->>Server: "tools/call {name: audit_url, arguments: {url}}"
    Server->>Handler: handleMcpRequest
    Handler->>Tools: callTool(audit_url, args)
    Tools->>Tools: normalizeUrl(url)
    Tools->>Tools: discover(target) + crawlPages()
    Tools->>Tools: buildRemoteReport() / formatRemoteReport()
    Tools-->>Handler: textResult(formattedReport)
    Handler-->>Server: "{result: {content, isError}}"
    Server-->>Client: JSON-RPC response
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
src/core/mcp-server.ts:264
`process.stdin` missing error handler — unhandled `error` event crashes the server process.

Only `data` and `end` events are registered on stdin. Node.js EventEmitters throw when an `error` event fires with no listener, terminating the process. This can happen when an MCP client exits abnormally (broken pipe on the write end of stdin before EOF is delivered). For a long-running stdio server that is supposed to survive client restarts, this is a live crash path.

```suggestion
  process.stdin.on('error', (err: NodeJS.ErrnoException) => {
    process.stderr.write(`[aeo.js mcp] stdin error: ${err.code ?? err.message}\n`);
    process.exit(1);
  });
  process.stdin.on('end', () => process.exit(0));
```

Reviews (8): Last reviewed commit: "fix(mcp): use type instead of interface ..." | Re-trigger Greptile

Comment thread src/core/mcp-server.ts Outdated
Comment thread src/core/mcp-server.ts Outdated
Comment thread src/core/mcp-server.ts Outdated
…nt disconnect

Also pin protocolVersion to '2025-03-26' in the initialize handler instead
of echoing back whatever the client sent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rubenmarcus

Copy link
Copy Markdown
Member Author

@greptileai review

Comment thread src/core/mcp-server.ts
EPIPE is the common case (client disconnect) but other errors like EBADF
can occur on unusual stdio setups. Under Node 15+ a rethrown error from
inside a .then() chain becomes an unhandled rejection that terminates the
process. Log all stdout write errors to stderr so they're visible without
crashing the long-running server.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rubenmarcus

Copy link
Copy Markdown
Member Author

@greptileai review

Comment thread src/core/mcp-server.ts
Comment thread src/core/mcp-server.ts
Comment thread src/core/mcp-server.ts
…te_aeo_files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rubenmarcus

Copy link
Copy Markdown
Member Author

@greptile review

…ponse

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rubenmarcus

Copy link
Copy Markdown
Member Author

@greptile review

Comment thread src/core/mcp-server.ts
Comment on lines +143 to +144
const cwd = process.cwd();
const resolved = isAbsolute(args.outDir) ? args.outDir : resolve(cwd, args.outDir);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Absolute outDir bypasses the cwd containment check. isAbsolute(args.outDir) ? args.outDir : ... preserves the raw string without normalizing .. segments, so a path like /home/user/project/../../../etc/cron.d will satisfy startsWith(cwd + '/') lexically while resolving to a location entirely outside cwd. Calling resolve() on the raw path — whether it's absolute or relative — removes all .. sequences before the comparison.

Suggested change
const cwd = process.cwd();
const resolved = isAbsolute(args.outDir) ? args.outDir : resolve(cwd, args.outDir);
const cwd = process.cwd();
const resolved = resolve(args.outDir);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/core/mcp-server.ts
Line: 143-144

Comment:
Absolute `outDir` bypasses the `cwd` containment check. `isAbsolute(args.outDir) ? args.outDir : ...` preserves the raw string without normalizing `..` segments, so a path like `/home/user/project/../../../etc/cron.d` will satisfy `startsWith(cwd + '/')` lexically while resolving to a location entirely outside `cwd`. Calling `resolve()` on the raw path — whether it's absolute or relative — removes all `..` sequences before the comparison.

```suggestion
        const cwd = process.cwd();
        const resolved = resolve(args.outDir);
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread src/core/mcp-server.ts
}
});

process.stdin.on('end', () => process.exit(0));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 process.stdin missing error handler — unhandled error event crashes the server process.

Only data and end events are registered on stdin. Node.js EventEmitters throw when an error event fires with no listener, terminating the process. This can happen when an MCP client exits abnormally (broken pipe on the write end of stdin before EOF is delivered). For a long-running stdio server that is supposed to survive client restarts, this is a live crash path.

Suggested change
process.stdin.on('end', () => process.exit(0));
process.stdin.on('error', (err: NodeJS.ErrnoException) => {
process.stderr.write(`[aeo.js mcp] stdin error: ${err.code ?? err.message}\n`);
process.exit(1);
});
process.stdin.on('end', () => process.exit(0));
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/core/mcp-server.ts
Line: 264

Comment:
`process.stdin` missing error handler — unhandled `error` event crashes the server process.

Only `data` and `end` events are registered on stdin. Node.js EventEmitters throw when an `error` event fires with no listener, terminating the process. This can happen when an MCP client exits abnormally (broken pipe on the write end of stdin before EOF is delivered). For a long-running stdio server that is supposed to survive client restarts, this is a live crash path.

```suggestion
  process.stdin.on('error', (err: NodeJS.ErrnoException) => {
    process.stderr.write(`[aeo.js mcp] stdin error: ${err.code ?? err.message}\n`);
    process.exit(1);
  });
  process.stdin.on('end', () => process.exit(0));
```

How can I resolve this? If you propose a fix, please make it concise.

@rubenmarcus rubenmarcus merged commit cd31812 into feat/remote-url-check Jun 16, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant