turva-mcp
by erekola
README.md
# turva-mcp
<p align="center">
<a href="https://glama.ai/mcp/connectors/dev.turva/turva-mcp"><img src="https://glama.ai/mcp/connectors/dev.turva/turva-mcp/badges/score.svg" alt="turva-mcp on Glama"></a>
</p>
A public, read-only Model Context Protocol server for [turva.dev](https://turva.dev), written in TypeScript. 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:
```text
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:
```json
{
"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:
```sh
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](https://glama.ai/mcp/connectors/dev.turva/turva-mcp).
## Try a tool
Once connected, call `get_contact` with an empty argument object. In an MCP client SDK, the call is:
```js
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](src/index.ts):
```json
{
"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`:
```sh
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 (audit, advisory, implementation, agent operations and MCP server design) and pricing (three fixed list prices and two on request), plus one implementation add-on that carries a fixed price and is sold only with the audit it follows, and active security testing, an add-on to the audit priced by quote |
| `get_agent_readiness` | turva.dev's agent-readiness score, category scores, measurement date and a link to the scanner start page |
| `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 records 100/100, Level 5 Agent-Native on [isitagentready.com](https://isitagentready.com/), 98/100 on the [Internet.nl website test](https://internet.nl/site/turva.dev/) and 90/100 on its [email test](https://internet.nl/mail/turva.dev/). It also records that all 24 categories passed on [Hardenize](https://www.hardenize.com/report/turva.dev). The Hardenize reading is dated 2026-11-07 and the others 2026-10-29. These readings come from third parties. This server did not produce any of the scores.
## 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 main site serves the full signed MCP server card at [turva.dev/.well-known/mcp/server-card.json](https://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`.
The server also negotiates revision `2025-03-26`, which requires a server to accept JSON-RPC batches. This endpoint refuses every batch with `400` and error `-32600` on both lanes. One POST must not carry many `tools/call` requests past the rate limit.
A request is on the current lane only when `MCP-Protocol-Version` is exactly `2026-07-28`. A request without that header, with an older revision or with an unknown version is on the legacy lane for this purpose: the lane decides only the shape of the `id` in an error, and an unknown version is still refused. An error that the server answers before it has read a request id leaves out the `id` member on the current lane. On the legacy lane it sends `"id": null`, as JSON-RPC 2.0 requires. An `OPTIONS` preflight carries no version header and is answered as the legacy lane.
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`. Validation of the request body is answered first, with `-32602` or a tool error. Malformed `params` and an unknown tool name fall under it, and so does an argument named `__proto__` or an argument list of more than five keys or with a key longer than 64 characters. The same order holds for a request that is also malformed: an unknown protocol version or a wrong `Mcp-Method` on a request with malformed `params` or an unknown tool name receives that `-32602` or tool error, not the header error. A request without `"jsonrpc": "2.0"`, or with an id that is not a string or a safe integer, skips these checks, and the SDK answers it with `400` and error `-32600`. Any other unexpected argument is checked after the headers, and the SDK answers it with a tool error. A JSON-RPC notification is passed to the SDK without this header check. `@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. The `agents` version pinned in `package.json` declares all three MCP packages as peer dependencies at exact versions. Only `@modelcontextprotocol/client` is a required peer. `@modelcontextprotocol/sdk` and `@modelcontextprotocol/server` are marked optional in the `peerDependenciesMeta` of `agents` 0.33.0. They are listed directly because the Worker imports the server package and the lockfile pins the sdk package on purpose. Neither `@modelcontextprotocol/client` nor `@modelcontextprotocol/sdk` 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.
- 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` accepts an `Origin` whose hostname is `turva.dev`, whatever the scheme or port, and refuses every other origin with `403`. A present `Origin` must also be a serialized origin, so a value with a path, user information, a fragment or a trailing slash is refused with `403`, and so is an empty value. The response always carries `Access-Control-Allow-Origin: https://turva.dev`. A browser page on another scheme or port of `turva.dev` therefore passes this check, and the browser's own CORS check then refuses 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](SECURITY.md) or email [info@turva.dev](mailto: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:
- [turva-llms-txt-validator](https://www.npmjs.com/package/turva-llms-txt-validator) checks llms.txt structure and home-page discovery declarations. Its [browser version](https://turva.dev/llms-txt-validator) accepts any public domain.
- [markdown-parity-check](https://www.npmjs.com/package/markdown-parity-check) compares the main content of HTML and Markdown pages. Its [browser version](https://turva.dev/markdown-parity-check) checks turva.dev's own published pages only.
Both run with `npx`. Both need Node.js 22 or newer. Their [validator instructions](https://github.com/erekola/llms-txt-validator#quick-start) and [comparison instructions](https://github.com/erekola/markdown-parity-check#readme) explain the arguments and exit codes.
## Develop and test
The Worker needs Node.js `^22.18.0 || >=24.11.0`, the range in the `engines` field of `package.json`. The entry point is [src/index.ts](src/index.ts). There is no build step: Wrangler bundles the TypeScript when you run or deploy it, and Node runs the tests by stripping types.
```sh
npm ci
npm run dev
npm test
npm run typecheck
```
- `npm run dev` starts `wrangler dev` for a local copy of the Worker.
- `npm test` runs `node --experimental-strip-types --test` over `test/catalog.test.ts`, `test/claims.test.ts` and `test/protocol.test.ts`.
- `npm run typecheck` runs `tsc --noEmit` twice, once for `src` with `tsconfig.json` and once for `test` with `tsconfig.test.json`.
- The two `overrides` in `package.json` are deliberate. One keeps `@hono/node-server` on a patched release, as [SECURITY.md](SECURITY.md) explains, and the other points every copy of the Workers types at the direct dependency. Run `npm ci` and `npm audit` again before removing either.
[The workflow](.github/workflows/ci.yml) runs on every push and pull request with read-only permissions. It has one job, `test`, on Node.js 22, 24 and 26. The job runs `npm ci`, then `npm audit --omit=dev --audit-level=moderate`, then `npm test` and `npm run typecheck`. A runtime dependency with a moderate or worse advisory fails the build at the audit step. Nothing in the workflow deploys.
The last JavaScript files in this repository were the two test files. They moved to TypeScript on 20 October 2026, the day the site Worker was renamed, and `tsconfig.test.json` has type-checked them since then. The server source in `src/index.ts` was already TypeScript.
## 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`.
Run the checks from Develop and test, then deploy with your Cloudflare account:
```sh
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](https://github.com/erekola) at [turva.dev](https://turva.dev). Questions about the implementation can go to [info@turva.dev](mailto:info@turva.dev). I work in writing. turva.dev builds servers like this one for other sites as a service, [MCP server design](https://turva.dev/services#mcp-server-design).
The business registration is available in the [Finnish Business Information System](https://tietopalvelu.ytj.fi/yritys/3600281-7).
## License
[MIT](LICENSE).
This server cannot be deployed
Maintenance
ActivityActive
ResponsivenessNo recent community issues