Threat model: MCP endpoint
Scope: mcp.ainumbers.co, the Cloudflare Worker that runs deterministic kernels for AI agents. Written from the worker source (worker.mjs, wrangler.jsonc in the MCP worker repository) on . All threat models.
Assets
- Availability. The endpoint is public, keyless and free. Compute cost is the resource an abuser spends.
- Result integrity. A caller relies on the returned numbers and receipts being what the kernel computes.
- Caller inputs. Tool arguments can contain business figures. They are computed in memory; the worker writes none of them to its analytics dataset.
- Operational metadata. Tool name, outcome, latency, network operator and client name (see data handling).
Trust boundaries
- Any internet client to the Cloudflare edge to the Worker. No authentication exists, by design (
authentication: none). - Worker to its bundled kernels and static data (
data/,kernels/), generated at deploy time. - Worker to Cloudflare services: Analytics Engine (
ANALYTICS), rate-limit bindings, a message queue, a weekly cron and a durable workflow, all declared inwrangler.jsonc. - Browser callers: CORS answers a fixed allowlist of origins and otherwise falls back to
https://ainumbers.co(worker.mjs, CORS headers block).
Adversaries
- M1. A scraper or runaway agent that floods the endpoint or walks the tool list repeatedly.
- M2. A caller who sends oversized or malformed bodies to burn parse CPU.
- M3. A caller or intermediary who tries to learn what other callers sent.
- M4. A maintainer or dependency change that adds logging of inputs, or a wrong kernel result.
In scope and out of scope
In scope: resource exhaustion at the application layer; request-size abuse; cross-caller data exposure; accidental retention of inputs; unauthorised browser origins.
Out of scope: volumetric network attacks (left to the edge provider); a compromise of the Cloudflare account; the legal correctness of any model a kernel implements; the weekly self-check fixtures, which hold no user content.
Controls
| Threat | Control | Where it lives |
|---|---|---|
| M1: request flood | Four rate-limit bindings: per-IP 60/60 s before parsing, per-IP 30/60 s, global 300/60 s, and a separate global 1200/60 s bucket for static discovery calls so tool-list walks cannot starve compute | wrangler.jsonc (unsafe.bindings), rateLimitExceeded() in worker.mjs |
| M1: limiter silently absent | An unprovisioned binding reads as undefined and the function does not block. That is a known fail-open; the two-step rollout was chosen so the binding was proven live before it was relied on | comment above rateLimitExceeded(); no gate detects a missing binding |
| M2: oversized body | 1 MiB cap, checked on Content-Length first and again on the read text, returning 413 | MAX_REQUEST_BODY_BYTES in worker.mjs |
| M3: retention of inputs | The analytics writes carry tool name, network number, outcome, correlation id, latency and chain depth for tool calls, and client name, client version, user agent and network number for initialize. No argument or output field is passed to writeDataPoint | worker.mjs, the two ANALYTICS.writeDataPoint call sites |
| M3: IP address stored by us | The IP is used only as the rate-limit key. It is not written to the analytics dataset | rateLimitExceeded(); comments at both write sites record that the truncated IP hash was dropped |
| M3: stateless sessions | The Mcp-Session-Id issued at initialize is a random UUID echo token, accepted but never required, and keeps no server state | worker.mjs, initialize handler |
| M4: wrong kernel result | Canonical hash path, golden-parity and forbidden-hash lint over kernels | chaingraph/kernels/{lint-forbidden-hash,golden-parity.test,syntax-check}.mjs in the site repository |
| M4: duplicate tool names break the handshake | Name-uniqueness check before any tool is added | mcp-apps-poc/scripts/check-tool-names.mjs |
| Browser origin abuse | CORS reflects only allowlisted origins | ALLOWED_ORIGINS in worker.mjs |
UNTESTABLE claims
- "Stores no inputs or outputs." We found no gate that fails the build if a future change logs a tool argument. The statement is true of the call sites named above on the date at the top. It is held by code review.
- Platform logs.
wrangler.jsoncturns on persisted invocation logs. What Cloudflare puts in an invocation log, and for how long it keeps it, is set by Cloudflare and cannot be tested from this repository. The privacy policy refers to this as provider-default handling. - Absence of a limiter in production. Rate limits are configured in code, but nothing probes the live endpoint to confirm each binding is answering.
- The other worker-side explicit log lines (scheduled-job envelopes and handler errors via
console.logandconsole.error) log fixture and schedule data, not caller input, as far as we read the source. No test asserts it.
Residual risk
- Anyone can call the endpoint, so a determined abuser can consume the free-tier compute of the account. The limits slow this down; they do not remove it.
- The rate-limit key is the caller's IP, which the worker reads from a Cloudflare header for that purpose. Callers behind a shared address share a budget.
- Users who paste confidential figures into an agent send them to that agent's own provider first. This model covers only what reaches our endpoint.