Your site as a read-only MCP server
EN

Your site as a read-only MCP server

Last verified: July 27, 2026
10 min read
Guide
500+ WP projects
AI integration

Most guidance on the Model Context Protocol assumes you are building a server in front of a store or a database, something with orders and inventory. We did the less obvious thing: we turned a static marketing site into a live, read-only MCP server. Any MCP client can now POST to a single endpoint and ask what services exist or how to start an inquiry, in typed JSON-RPC, without parsing a single page of HTML.

This is a practitioner writeup of that build. It is deliberately narrow: two read-only tools, no database, no writes. That narrowness is the point, and most of the interesting decisions come from it. What follows is why a content site benefits from an MCP endpoint at all, why the endpoint is a Cloudflare Pages Function rather than a framework route, why we hand-rolled JSON-RPC instead of pulling the SDK, and the one trailing-slash bug that silently breaks every POST if you miss it.

#Why put an MCP server on a marketing site

A store exposes MCP because an agent needs to take an action: check stock, build a cart, place an order. A marketing site has no cart. So what is the tool surface for?

The honest answer is agent discovery. Our site already ran an agent-discovery layer: an llms.txt, a set of .well-known documents, and an MCP server card at /.well-known/mcp/server-card.json. That card described a server, its transport, and its capabilities. It even declared a tools capability. There was one problem: nothing lived behind it. The card advertised a server that did not exist. An agent that followed the card and tried to connect got nothing.

That is a common state for these files. They get generated, they get advertised in a Link header, and then they point at a resource that was never built. The card is a promise with no implementation.

So the goal was not to invent a new surface. It was to make an existing, advertised surface real. The tools follow directly from what a marketing site is for:

  • check_services returns the service catalogue: id, name, description, category, and canonical URL.
  • request_quote returns the localized contact URL and instructions to submit an inquiry.

Two tools, both read-only. An agent can now enumerate what we do and hand a user the right next step, deterministically, instead of guessing from prose.

#The architecture decision: a Pages Function, not an Astro route

The site is built with Astro in static output mode. There is no SSR adapter. Every page is prerendered to HTML at build time, and the API-style files it serves (the service catalogue JSON, the agent profile) are static files under public/, not server routes.

That single fact drives the whole design. A dynamic endpoint that reads a request body and returns a computed response cannot be an Astro route here, because there is no server to run it. Adding one would mean switching the output mode to hybrid or server and attaching an adapter, which is a large, risky change to a build that already produces thousands of pages.

The site does, however, already run hand-written Cloudflare Pages Functions: a middleware that handles redirects, and a few small API handlers. That is the seam. A dynamic MCP endpoint is just one more Pages Function, functions/mcp.ts, sitting next to the others. It deploys with the normal build, needs no adapter, and changes nothing about how the pages are rendered.

The lesson generalizes. When you want to add one dynamic endpoint to an otherwise static site, do not reach for the framework’s server mode. Reach for whatever edge-function primitive your host already gives you. On Cloudflare Pages that is a Function. The cost is one file, not a rebuild of your rendering model.

#Hand-rolled JSON-RPC beats the SDK for two tools

The reflex when you read “MCP server” is to install @modelcontextprotocol/sdk. For a stdio server with many tools, resources, and prompts, that reflex is correct. For two read-only tools on an edge function, it is not.

The MCP wire protocol is JSON-RPC 2.0. A server that answers an MCP client needs to handle a small set of methods:

  • initialize, where the server returns its protocol version, capabilities, and identity.
  • tools/list, where it returns the tool definitions with their JSON Schema.
  • tools/call, where it runs a named tool and returns the result wrapped in MCP content.
  • notifications like notifications/initialized, which carry no id and expect no response.

That is the entire surface for a read-only server. Implementing it by hand is roughly 120 lines. The dispatch is a plain switch:

switch (method) {
  case "initialize":
    return ok({ protocolVersion, capabilities: { tools: { listChanged: false } }, serverInfo });
  case "tools/list":
    return ok({ tools: TOOLS });
  case "tools/call":
    return ok({ content: [{ type: "text", text: callTool(name, args, services).text }] });
  default:
    return err(-32601, `Method not found: ${method}`);
}

Weigh that against the SDK. The SDK is designed for stdio and for the full protocol. On an edge runtime you inherit its bundling assumptions and its transport machinery for features you are not using. For two read tools, a dependency you have to reason about is a worse trade than 120 lines you fully control. This is the same read-only-first restraint applied to dependencies: expose the minimum, own the minimum.

The one thing worth doing carefully by hand is the error contract. JSON-RPC has defined error codes, and MCP clients expect them: -32700 for a parse error, -32601 for an unknown method, -32602 for bad params. Getting those right is what makes a hand-rolled server behave like a real one to a strict client.

#Read-only first is a safety decision, not a limitation

The tempting third tool is one that captures a lead: take a name, an email, and a message, and email it to us. Do not build that as an open MCP tool.

A public endpoint with a writable tool is a spam sink. Anyone on the internet can call tools/call with submit_quote and a fake payload, and now your inbox, your CRM, or your database is a target with no friction and no human in the loop. The moment a tool writes, the endpoint needs authentication, rate limiting, and abuse handling, which is a large surface for a marketing site to defend.

So request_quote writes nothing. It returns the localized contact URL and structured guidance:

if (name === "request_quote") {
  const lang = LOCALES.includes(String(args?.lang)) ? String(args.lang) : "en";
  return { text: JSON.stringify({
    contact_url: CONTACT_URLS[lang],
    method: "web-form",
    note: "Read-only endpoint. Submit the inquiry through the contact form; this tool does not send it for you.",
    reply_time: "within one working day",
  }, null, 2) };
}

The agent gets everything it needs to move the user forward: the right contact page for the user’s language, and a clear statement that submission happens through the form. The human stays in the loop. The endpoint has nothing to abuse.

This is not a weaker design forced by caution. It is the same stance we recommend to clients building MCP for their own systems: read-only first, add writes only behind authentication when there is a concrete reason. A read-only server is honest about what it is, and it is safe to leave open.

Design choiceRead-write open toolRead-only hand-off
Auth requiredYesNo
Spam surfaceHighNone
Human in the loopOptionalAlways
Code to defendRate limit, validation, abuse handlingNone
Fit for a public marketing sitePoorGood

#The trailing-slash trap that drops your POST body

This one cost real debugging time, so it is worth stating plainly.

Our site canonicalizes URLs to a trailing slash. The middleware 301-redirects any path without one to the slashed version. That is fine for pages. It is a silent disaster for a JSON-RPC endpoint.

A POST /mcp hits the redirect and returns 301 to /mcp/. Many HTTP clients, following the redirect, do not resend the POST body, or downgrade the method. The MCP client sees an empty or failed response and concludes the server is broken. The server is fine. The request never arrived with its body.

The fix is not to fight the middleware. It is to advertise the URL the site actually serves. The MCP server card and every discovery reference point at /mcp/, with the trailing slash, so a well-behaved client POSTs directly to the canonical URL and never touches the redirect. If your framework or host normalizes slashes, decide which form is canonical and advertise exactly that form everywhere.

You can verify the live endpoint the same way we do:

curl -s -X POST https://wppoland.com/mcp/ \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

A slashless POST to the same host will show you the 301 instead.

#How this fits agent-readiness and GEO

Generative engine optimization is mostly about being legible and verifiable to models. Structured data, clear entities, consistent claims across your own surfaces and external ones. A live MCP endpoint is a strong version of that legibility: it is not a hint about your content, it is a callable interface to it.

Two things make it worth the small effort even while adoption is early. First, it closes the gap between what the discovery layer advertises and what exists. A card that points at a working server is a coherent signal; a card that points at nothing is a broken one, and broken signals are worse than absent ones. Second, for anyone who sells this capability, the endpoint is proof. We build MCP servers for clients, and the most credible demonstration of that is a public, read-only one running on our own domain, next to the open-source WooCommerce MCP server we publish.

No major AI provider formally commits to reading MCP server cards or llms.txt today. That is a fair caveat and we state it plainly. The endpoint is cheap to run, it makes an advertised promise true, and it is a working artifact rather than a claim. Those three together clear the bar.

#What we deliberately left out

A short list, because knowing what a build skips is as useful as knowing what it includes.

  • No get_case_studies tool yet. An agent can read the case studies from llms.txt and the linked pages. We add the tool when a real client asks for it, not before.
  • No resources or prompts capability. The server declares only tools, because that is all it implements. Declaring a capability you do not serve is worse than omitting it: a client that calls resources/list gets an error instead of a clean “not supported”.
  • No SDK, as covered above.
  • No writes, as covered above.

Each omission is a decision, not an oversight. The endpoint does exactly what the two intents require and nothing more, which is why it is small enough to reason about in one sitting and safe enough to leave open to the internet.

#Where to go next

If you want the fuller version of these decisions, the cluster around this post covers the adjacent ground: building an MCP server for WooCommerce for the stateful, store-facing case, MCP vs REST: when each wins for whether you need MCP at all, and MCP authentication patterns for the moment you do add a writable tool and need auth. The commercial version of this work lives on the MCP server development service page.

The short version fits in a sentence: a marketing site can be an MCP server, the endpoint is one edge function, keep every tool read-only, and advertise the URL your host actually serves.

Next step

Turn the article into an actual implementation

This block strengthens internal linking and gives readers the most relevant next move instead of leaving them at a dead end.

Want this implemented on your site?

If visibility in Google and AI systems matters, I can build the content architecture, FAQ, schema, and internal linking needed for SEO, GEO, and AEO.

Related cluster

Explore other WordPress services and knowledge base

Strengthen your business with professional technical support in key areas of the WordPress ecosystem.

What does site-as-MCP mean for a marketing site?#
It means the site answers Model Context Protocol calls directly. Instead of an agent scraping HTML, it calls typed tools like check_services and request_quote over JSON-RPC. The site becomes a machine-readable surface, not just a set of pages.
Why a Cloudflare Pages Function instead of an Astro route?#
The site builds as static output with no SSR adapter, so there is no server route to attach an endpoint to. A Pages Function is a small standalone handler that deploys with the same build and runs at the edge, with no change to the output mode.
Do you need the official MCP SDK?#
Not for a handful of read tools. The wire protocol is JSON-RPC 2.0 with three methods that matter here: initialize, tools/list, and tools/call. Implementing them by hand is about 120 lines and avoids a dependency and its bundling quirks.
Is a public MCP endpoint a security risk?#
Only if a tool writes. Keeping every tool read-only removes the risk. The request_quote tool returns the contact URL and submission instructions rather than sending anything, so there is no spam-writable lead sink exposed to the internet.
Does any AI provider actually read these endpoints?#
No major provider formally commits to consuming MCP server cards or llms.txt yet. They appear in server logs often enough to justify the small cost, and the endpoint doubles as verifiable proof of the capability we ship for clients.

Need an FAQ tailored to your industry and market? We can build one aligned with your business goals.

Let’s discuss

Related Articles