A public, read-only Model Context Protocol server for turva.dev. It lets an AI client look up the site's published information, including contact details and dated measurement evidence.
For example, a client can use get_contact when someone asks how to reach me. The reply explains how to start. The server answers questions about turva.dev itself. It does not scan another domain, run an audit or perform transactions.
Streamable HTTP endpoint:
https://mcp.turva.dev/mcp
Connect to the hosted server directly. No npm installation, authentication or API key is required. Set the endpoint in an MCP client with Streamable HTTP support. Clients that use a URL-based mcpServers configuration commonly accept this shape:
{
"mcpServers": {
"turva": {
"url": "https://mcp.turva.dev/mcp"
}
}
}The endpoint expects MCP POST requests. Opening /mcp in a browser returns 405. Check reachability through the discovery document instead:
curl https://mcp.turva.dev/In Windows PowerShell, use curl.exe if curl resolves to Invoke-WebRequest.
The server is listed in the official MCP registry as dev.turva/turva-mcp and in the Glama MCP directory.
Once connected, call get_contact with an empty argument object. In an MCP client SDK, the call is:
const result = await client.callTool({ name: "get_contact", arguments: {} });
const contact = result.structuredContent;
console.log(contact.email, contact.first_reply);Here client is your connected MCP client. The same JSON is also in result.content[0].text for clients that read text only. This excerpt from the response shows the contact fields maintained in src/index.ts:
{
"email": "info@turva.dev",
"location": "Tampere, Finland",
"engagement": "async_only",
"first_reply": "Within one business day, in writing, by email or Signal. LinkedIn messages have no set reply time."
}The full response also includes the other contact channels, the steps for starting an engagement and an operator object that says who runs turva.dev.
Without an SDK, a request on the current protocol lane needs four things beside the JSON-RPC body: the MCP-Protocol-Version header, the Mcp-Method header, the Mcp-Name header on tools/call, and a _meta object in params that names the protocol version and the client's capabilities. The same call with curl:
curl https://mcp.turva.dev/mcp \
-H "Content-Type: application/json" \
-H "MCP-Protocol-Version: 2026-07-28" \
-H "Mcp-Method: tools/call" \
-H "Mcp-Name: get_contact" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_contact","arguments":{},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'The answer is one JSON-RPC response, and the contact data is in result.structuredContent.
Five read-only tools, each idempotent. Each returns its data as structuredContent, which the tool's outputSchema describes, and as the same JSON in a text block. No tool takes arguments: the input schema allows none, and a call that passes one returns a tool error. There are no write tools or transaction tools.
| Tool | Returns |
|---|---|
get_services |
The engagement model, the service catalog (Shopify agent storefront check, audit, advisory, implementation, agent operations and MCP server design) and pricing (four fixed list prices and two on request), plus two implementation add-ons that carry a fixed price and are sold only with the diagnosis they follow |
get_agent_readiness |
turva.dev's agent-readiness score, category scores, measurement date and verification link |
get_security_evidence |
Public Hardenize and Internet.nl results for turva.dev, with their measurement date |
get_principles |
The principles covering written work, access and verification |
get_contact |
Who runs turva.dev, the contact channels, the first-reply time and what access an audit needs |
Tool responses come from static TypeScript objects bundled with the Worker. They do not depend on a live upstream request. The measurement tools include a measured_at date and public verification links, so compare the recorded values with a fresh scan when current status matters.
The bundled snapshot dated 2026-09-23 records 100/100, Level 5 Agent-Native on isitagentready.com, all 24 categories passed on Hardenize, 98/100 on the Internet.nl website test and 90/100 on its email test. These are third-party readings of turva.dev, not scores produced by this server.
| Method and path | Behavior |
|---|---|
POST /mcp |
MCP over Streamable HTTP. A body over 64 KiB receives 413, and a JSON-RPC batch receives 400 with error -32600 |
GET /mcp, DELETE /mcp |
405 with Allow: POST, OPTIONS. No GET stream or session teardown |
OPTIONS /mcp |
200 for an accepted MCP preflight, 403 when the browser Origin is not allowed |
/mcp/ |
308 to /mcp, keeping the method and the query string. A preflight OPTIONS is answered as the one for /mcp, because a browser does not follow a redirect on a preflight |
GET / |
Minimal discovery JSON with the server name, transport and endpoint |
GET /.well-known/mcp |
The same discovery JSON |
GET /.well-known/glama.json |
Glama domain-verification document |
OPTIONS on any other path |
204 discovery CORS preflight |
POST, PUT, DELETE or PATCH on any path other than /mcp and /mcp/ |
405 with Allow: GET, HEAD, OPTIONS |
GET or HEAD on any other path |
404 |
HEAD on a discovery document returns its headers without a body. The discovery documents carry Cache-Control: public, max-age=3600, the same hour that tools/list and server/discover declare.
The full signed MCP server card is published at turva.dev/.well-known/mcp/server-card.json.
A single Cloudflare Worker built on the Cloudflare Agents SDK serves the endpoint through createMcpHandler from agents/mcp/server, and it creates a fresh McpServer from @modelcontextprotocol/server for each request. There is no Durable Object or persistent MCP session.
The current protocol lane uses revision 2026-07-28. A legacy lane at the same endpoint serves 2025-era clients. That lane is an adapter in the Agents SDK, and it answers through the web-standard server transport of @modelcontextprotocol/server. On the current lane every request must carry MCP-Protocol-Version and Mcp-Method, plus Mcp-Name for tools/call, and a request without one of them receives 400 with error -32020. @modelcontextprotocol/server checks Mcp-Method and Mcp-Name, and the Worker checks MCP-Protocol-Version before the handler runs, because that package reads the version from the request body and would otherwise answer a request that lacks the header. On the current lane nothing checks Accept, while the legacy lane answers 406 unless Accept lists both application/json and text/event-stream. Standard MCP clients handle these details.
server/discover is supplied by @modelcontextprotocol/server. It declares the tools capability with listChanged: false, because the tool set changes only on deploy and the server sends no change notifications, and it returns short instructions that say which tool answers which question.
The discovery documents and tool data are compiled into the Worker. This Worker is separate from the main turva.dev Worker, so changes here do not change the website.
The code imports @modelcontextprotocol/server, agents and zod. package.json also lists @modelcontextprotocol/client and @modelcontextprotocol/sdk, which the code does not import. agents 0.23.0 declares all three MCP packages as required peer dependencies at exact versions, so npm has to install them, but neither of the two is in the built Worker.
This server has no write path and returns only public data, so what it has to withstand is abuse of the endpoint itself: load from one address, requests built to be expensive, and a page on another site that calls it from a visitor's browser. The list below says how the endpoint meets them.
- Public and unauthenticated by design. Every exposed value is already public.
- Read-only MCP annotations on every tool. No destructive or open-world operation is declared, and no tool takes arguments.
- Rate limit: about 100 requests per 60 seconds per client IP, with
429andRetry-After: 60after it. Cloudflare's rate-limiting binding keeps a separate, approximate count in each location, so a burst can pass more requests before the first429. The endpoint fails open if the rate-limiter binding is missing or errors. - Request size: a body over 64 KiB receives
413before the handler reads it, and a JSON-RPC batch receives400on both lanes, so one request cannot carry many tool calls past the rate limit. - Browser CORS on
/mcpallows onlyhttps://turva.devasOrigin. Other origins receive403. The check compares the hostname, so the scheme and the port are not part of it. Non-browser MCP clients normally send noOriginheader and can connect directly. Discovery documents use open CORS so directories can read them. - The code does not store request bodies, client identities or tool inputs. Cloudflare Workers observability is disabled. A rate-limiter failure writes a diagnostic error without request data.
- Security headers are applied to MCP and discovery responses.
For private vulnerability reports, see SECURITY.md or email info@turva.dev.
The two checks below are separate tools. They are not MCP tools, and this server does not run them. For checks against another website, use the standalone npm packages:
- turva-llms-txt-validator checks llms.txt structure and home-page discovery declarations. Its browser version accepts any public domain.
- markdown-parity-check compares the main content of HTML and Markdown pages. Its browser version checks turva.dev's own published pages only.
Both run with npx. The validator needs Node.js 18.17 or newer, and the comparison needs Node.js 22 or newer. Their validator instructions and comparison instructions explain the arguments and exit codes.
This repository is MIT licensed and can be adapted for another site. Before deploying a fork:
- Replace the static service and evidence objects in
src/index.tswith your own published data. - Replace the hard-coded
turva.devdomain, MCP endpoint, browser origin and verification links with values you control. - Give the Worker a unique
nameand a rate-limitnamespace_idthat is not shared with another Worker in your Cloudflare account. - Attach your own custom domain.
workers_devis disabled inwrangler.jsonc.
Use Node.js 22 or 24, matching the repository's CI. From the root of your clone, install dependencies, run the tests and check the types before deploying with your Cloudflare account:
npm ci
npm test
npm run typecheck
npm run deployConfigure the custom domain under Workers & Pages, your Worker, Settings, Domains & Routes. Use your own hostname and update the discovery endpoint in src/index.ts to match it.
Built by Erik Rekola at turva.dev. Questions about the implementation can go to info@turva.dev. I work in writing. turva.dev builds servers like this one for other sites as a service, MCP server design.
The business registration is available in the Finnish Business Information System.
MIT.