Skip to content

Repository files navigation

turva-mcp

turva-mcp on Glama

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.

Connect

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.

Try a tool

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.

Tools

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

Evidence

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.

Endpoints

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.

Protocol and implementation

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.

Dependencies

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.

Security and operating limits

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 429 and Retry-After: 60 after it. Cloudflare's rate-limiting binding keeps a separate, approximate count in each location, so a burst can pass more requests before the first 429. The endpoint fails open if the rate-limiter binding is missing or errors.
  • Request size: a body over 64 KiB receives 413 before the handler reads it, and a JSON-RPC batch receives 400 on both lanes, so one request cannot carry many tool calls past the rate limit.
  • Browser CORS on /mcp allows only https://turva.dev as Origin. Other origins receive 403. The check compares the hostname, so the scheme and the port are not part of it. Non-browser MCP clients normally send no Origin header 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.

Related tools outside this server

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:

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.

Deploy your own copy

This repository is MIT licensed and can be adapted for another site. Before deploying a fork:

  1. Replace the static service and evidence objects in src/index.ts with your own published data.
  2. Replace the hard-coded turva.dev domain, MCP endpoint, browser origin and verification links with values you control.
  3. Give the Worker a unique name and a rate-limit namespace_id that is not shared with another Worker in your Cloudflare account.
  4. Attach your own custom domain. workers_dev is disabled in wrangler.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 deploy

Configure 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.

Maintainer

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.

License

MIT.

About

Read-only MCP tools for turva.dev's published information and evidence. Streamable HTTP on Cloudflare Workers, with no API key required.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages