How I made my website fully agent-readable: an MCP server + NLWeb /ask in Next.js
Most websites are built for humans with browsers. Increasingly, the visitor is an AI agent — Claude, a custom assistant, some autonomous tool — and it doesn't want your hero animation. It wants structured, machine-readable answers and actions it can take.
I rebuilt my own site, yonyon.ai, to be readable and usable by agents end to end: a live /ask endpoint (Microsoft's NLWeb shape), a real MCP server at /mcp, in-browser WebMCP tools, plus llms.txt and a .well-known/ discovery tree. This post is the worked example — the three pieces that did the heavy lifting, with the actual code.
The trap: locale routing eats your agent endpoints
My site is internationalized with next-intl. Its middleware matches every path and routes it into a [locale] segment. So the moment I added a bare /ask route for agents, the i18n middleware grabbed it first and tried to render /[locale]/ask — 404. Same problem would hit /mcp.
Agents expect bare, unprefixed paths (/ask, /mcp) — not /en/ask. The fix is to intercept those paths in the proxy/middleware, before the i18n matcher runs, and rewrite them onto the real API route. rewrite() (not redirect()) preserves the HTTP method and body, so a POST /ask stays a POST:
// src/proxy.ts (next-intl middleware entrypoint)
import createMiddleware from "next-intl/middleware";
import { NextResponse, type NextRequest } from "next/server";
import { routing } from "./i18n/routing";
const intlMiddleware = createMiddleware(routing);
export default function proxy(request: NextRequest) {
// Rewrite the bare /ask onto the /api/ask route handler BEFORE next-intl
// pulls it into the [locale] tree (which would 404). rewrite() keeps the
// method + body, so POST /ask works.
if (request.nextUrl.pathname === "/ask") {
return NextResponse.rewrite(new URL("/api/ask", request.url));
}
return intlMiddleware(request) as NextResponse;
}
export const config = {
// Skip /api, /_next, static files, etc. — only run on page-ish paths.
matcher: "/((?!api|studio|trpc|_next|_vercel|.*\\..*).*)",
};
That's the whole alias trick: bare agent paths are matched first and rewritten into /api/*, so they never reach the locale router. Add a line per endpoint you want to expose unprefixed.
A dependency-free MCP server
The Model Context Protocol is how agents discover and call your tools. The official SDK is great — but it peer-depends on zod 3, and my repo is on zod 4. Rather than fight the dependency tree, I hand-rolled the handler. MCP's Streamable-HTTP transport is just JSON-RPC 2.0 over POST, and I only needed a handful of methods: initialize, tools/list, tools/call, ping. Zero dependency risk:
// src/app/api/mcp/route.ts
export const dynamic = "force-dynamic";
const SERVER_INFO = { name: "yonyon.ai", version: "1.0.0" };
const PROTOCOL_VERSION = "2025-06-18";
const TOOLS = [
{
name: "ask_yonatan",
description: "Ask about Yonatan Gross's work, projects, or experience.",
inputSchema: {
type: "object",
properties: { question: { type: "string", maxLength: 2000 } },
required: ["question"],
additionalProperties: false,
},
// Behavioural hints so agents can reason about side effects:
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
},
// browse_projects, book_intro_call ...
];
const rpc = (id, result) => Response.json({ jsonrpc: "2.0", id, result });
const rpcError = (id, code, message) =>
Response.json({ jsonrpc: "2.0", id, error: { code, message } });
export async function POST(req: Request) {
const { id, method, params } = await req.json();
if (method === "initialize")
return rpc(id, { protocolVersion: PROTOCOL_VERSION, capabilities: { tools: {} }, serverInfo: SERVER_INFO });
if (method === "tools/list") return rpc(id, { tools: TOOLS });
if (method === "tools/call") {
const { name, arguments: args } = params;
if (!TOOLS.some((t) => t.name === name))
return rpcError(id, -32602, `Unknown tool: ${name}`); // structured error, not soft text
return rpc(id, await callTool(name, args));
}
if (method === "ping") return rpc(id, {});
return rpcError(id ?? null, -32601, `Method not found: ${method}`);
}
Two details that matter for agent ergonomics:
-
Tool annotations (
readOnlyHint,destructiveHint,openWorldHint) let a planner decide whether a tool is safe to call autonomously. - An unknown tool returns a structured JSON-RPC error (
code+message), not a friendly text blob — so agents branch on it programmatically instead of string-matching.
Discovery: llms.txt + .well-known
Endpoints are useless if nothing can find them. Two conventions cover it:
/llms.txt — a human- and agent-readable map of the site, modeled on robots.txt but for LLMs: who you are, what an agent can do here, and the exact endpoints:
# yonyon.ai — Yonatan Gross
> AI Platform Engineer & Backend Developer. Builds production AI systems
> end-to-end: RAG pipelines, multi-agent architectures, MCP servers.
## What an agent can do here
- Ask about my work — POST /ask with {"query":"..."} (NLWeb, no auth)
- Browse projects, book a free 15-min intro call
- MCP server at /mcp (initialize, tools/list, tools/call)
The .well-known/ tree carries the machine-readable specs:
public/.well-known/
├── mcp.json # points agents at /mcp
├── mcp/server-card.json # MCP server identity card
├── openapi.json # OpenAPI for the HTTP endpoints
├── api-catalog # RFC 9727 linkset of all APIs
└── agent-skills/ # discrete "skills" (ask, browse, book) as SKILL.md
Because these live in public/, they're served as plain static files — bypassing the locale router entirely, so there's no dotpath/404 issue. I also emit RFC 8288 Link headers (rel="describedby" → llms.txt, rel="service-desc" → openapi.json) so an agent that only fetches headers still discovers everything.
The payoff
You can verify it live:
curl -s https://yonyon.ai/ask \
-H 'content-type: application/json' \
-d '{"query":"What does Yonatan build?"}'
That single-shot, no-auth, agent-facing endpoint returns a grounded answer from the same RAG-backed model that powers the site chat. An agent can then call the MCP book_intro_call tool and act.
Building agent-ready surfaces is also just good practice for the corpus AI models train on — the more structured and citable your presence, the more likely a model recalls your work. I package these patterns (and a lot more) as OrchestKit, my open-source Claude Code agent framework — 111 skills, 37 agents, 211 hooks.
If you're making something agent-readable and want a second pair of eyes, the front door at yonyon.ai is itself the demo — ask it a question, or grab a free 15-minute intro call.
Top comments (0)