feat: MCP server — let agents audit sites and generate AEO files#66
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Docs PreviewPreview URL: https://feat-mcp-server.aeojs.pages.dev This preview was deployed from the latest commit on this PR. |
Greptile SummaryAdds a zero-dependency MCP server (
Confidence Score: 4/5Safe 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 src/core/mcp-server.ts — stdin error handler and Windows path separator in the outDir guard. Important Files Changed
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
%%{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
Prompt To Fix All With AIFix 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 |
…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>
|
@greptileai review |
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>
|
@greptileai review |
…te_aeo_files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@greptile review |
…ponse Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@greptile review |
| const cwd = process.cwd(); | ||
| const resolved = isAbsolute(args.outDir) ? args.outDir : resolve(cwd, args.outDir); |
There was a problem hiding this 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.
| 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.| } | ||
| }); | ||
|
|
||
| process.stdin.on('end', () => process.exit(0)); |
There was a problem hiding this 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.
| 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.
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 mcpruns 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
initialize,ping,tools/list,tools/call, notifications ignored) — no SDK dependency added, keeping the package lean.serverInfo: theVERSIONconstant said 0.0.13 while package.json is 0.0.14 (--versionwas wrong too).features/mcppage + sidebar entry.Verification
tsc --noEmitclean; 220 tests pass (10 new: initialize/ping/tools-list, citability + audit tool calls with mocked fetch, error paths, notification handling)initializeandtools/listround-trip correctly🤖 Generated with Claude Code