Build It Yourself

Clone this MCP server

For engineers who have used the MCP Playground against mcp.ainumbers.co and want to stand up an equivalent server of their own: hundreds of deterministic workflows exposed as MCP tools, running on Cloudflare Workers, with no runtime database and no build server.

Scope. This is a description of a working setup, written from its own sources, not a generic MCP tutorial. Names of scripts, gates and config keys below are the real ones. Nothing here is a hosted product you sign up for, and none of it requires a paid Cloudflare plan for the shape described. Adapt the specifics to your own catalog; the failure modes are the part worth copying.

1. What you are cloning

The thing behind https://mcp.ainumbers.co/mcp is a single Cloudflare Worker. It speaks MCP over streamable HTTP, registers several hundred tools, and answers tools/call by running a deterministic JavaScript kernel over the arguments it was given. Tool definitions and kernels are not authored inside the Worker: they are generated from a catalog file that lives in a separate repository and are copied in as static files before deploy.

What it is not, and this shapes every decision below:

If your catalog is small, most of what follows is overkill. The interesting parts start at roughly one hundred tools, where per-request schema construction and bundle size both stop being free.

2. The two-repo shape, and why

Two repositories, deliberately:

RepoDefault branchHolds
PostOakLabs/ainumbersmainSource of truth. The catalog (chaingraph/chaingraph.json), every kernel module, tool manifests, and the static site itself. Zero npm dependencies.
PostOakLabs/ainumbers-mcp-appsmasterThe deployable Worker: worker.mjs, wrangler.jsonc, CI, and the vendored copies of everything the Worker needs at runtime.

The split exists because the two halves have opposite dependency profiles. The site repo is a zero-dependency static estate that must stay installable-free and auditable. The Worker repo needs the MCP SDK, Wrangler, and a dependency bot keeping them current. Keeping them in one repository would push npm into the site, or push a build step into the pages.

The cost of the split is a copy step, which is the single largest source of outages in this design. Section 3 is that copy step, and section 4 is the gate that catches its worst failure.

3. The generation step, and the rule that prevents outages

A script in the Worker repo, generate.mjs, reads the site checkout and writes into the Worker repo:

It resolves the site checkout explicitly, in this order: a --repo= flag, then the AINUMBERS_REPO environment variable, then ../repo relative to the invoking shell's working directory. Deriving that path from the script's own location was tried and removed: run from an isolated worktree, it silently resolved to the wrong checkout and writes were discarded.

# from the Worker repo, with the site checkout beside it
node generate.mjs --repo=/path/to/ainumbers
The rule. Edit the catalog or a kernel, re-run generate.mjs, and commit data/ and kernels/ in the same push. The cloud build cannot run generation, because the sibling site checkout does not exist there. A push that changes the catalog without the regenerated output deploys a stale or unresolvable bundle. This one rule accounts for the recurring class of "the server went down right after a merge".

Two gates make the rule enforceable rather than aspirational. A dry-run bundle fails if any static import cannot resolve in the committed tree, which catches the case where kernels/ was never committed. A vendor-freshness check re-reads the site repository at HEAD in CI and compares it semantically against the committed vendored copies, which catches the case where they were committed but are stale.

4. Unique tool names, or the handshake dies

The MCP SDK throws when the same tool name is registered twice. That throw happens inside server construction, so it does not break one tool: it returns a 500 for initialize and the entire server becomes unusable to every client. A bundle can be perfectly valid and still do this.

So name uniqueness is a build gate, not a convention. The check reads the vendored data, meaning what will actually deploy, and asserts a single namespace across three sources:

node generate.mjs --repo=/path/to/ainumbers
node scripts/check-tool-names.mjs   # exit 1 on a fatal collision

Two details worth stealing. First, that utility list used to be a second hardcoded copy inside the checker and it drifted, so a new utility tool could ship having never been collision-checked; importing from the producing module removes the possibility. Second, not every collision is fatal: a widget and a catalog node that are deliberately the same tool resolve through a dedup in the Worker, so the checker exits zero and warns, rather than blocking a legitimate pairing.

5. Cloudflare workarounds

Exactly one deployer

Cloudflare's native Git integration (Workers Builds) and a GitHub Actions deploy job will both fire on the same push, producing two versions seconds apart. The Workers Build is ungated, so it can ship a bundle that the Actions pipeline would have rejected. This is not theoretical: it was decided once, regressed when the integration was reconnected, and took the endpoint down.

Keep Workers Builds disconnected. Deploy from CI only, gated on the validation job. If the endpoint breaks right after a push, check first whether a Workers Build got reconnected in the dashboard under the Worker's build settings. Also avoid hand-running wrangler deploy against production, for the same reason: it bypasses every gate.

The rate-limit rule turns smoke tests into skips

A WAF rate-limit rule protects the /mcp path (the Playground states the live figure: 50 requests per 10 seconds per IP). Any smoke test that sweeps the whole catalog exceeds that in a burst, and the responses come back as HTTP 429 or 503. The trap is how those surface: a sweep script that treats a non-answer as "this vector could not run" reports them as skips, not failures, and a run full of skips still exits zero.

Three mitigations, all of which the real scripts use:

Fitting hundreds of tools inside a free-plan CPU budget

Registering several hundred tools means the SDK converts every tool's schema to JSON Schema. Doing that per request on a cold isolate exceeds the per-invocation CPU limit and Cloudflare returns error 1102. Three structural fixes, in order of how much they buy:

  1. Precompute discovery. The initialize, tools/list, resources/list and prompts/list responses are immutable for a given deploy. They are captured once at generation time by driving the real server through an in-memory transport at the raw JSON-RPC layer, then served as static bytes. The server is never rebuilt for discovery.
  2. Build one tool for a call. tools/call builds the server with an "only this tool" option, skipping schema construction for everything else. A CI gate asserts that the single-tool build registers that tool byte-identically to the full build, so the optimization cannot silently change a definition.
  3. Serve large data as assets, not bundle. The search index is measured in megabytes and lives under the assets directory, served through the assets binding with run_worker_first set, rather than being imported into the bundle. Re-parsing a large list on the cold-start hot path was itself an outage cause and is now blocked by a static invariant check.
Never cache and reuse the server object. Holding an McpServer in module scope across requests produces "Already connected to a transport" and a 500 on the second request into a warm isolate. Construct per request. A gate greps for the pattern deterministically, because a local dev server does not reproduce the isolate reuse that triggers it.

Request-shape quirks

6. CI gate order

One validation job runs on every pull request and every push. A separate deploy job runs only on a push to the default branch and only if validation passed. The order that matters:

  1. Dry-run bundle. wrangler deploy --dry-run bundles the Worker without deploying it, failing if any static import cannot resolve in the committed tree.
  2. Tool-name collision check. The gate from section 4.
  3. Worker hot-path invariants. Static checks for the specific patterns that caused past outages: caching the server object, re-parsing large lists on the cold path, eager asset loading.
  4. Kernel coverage. Every catalog node that claims server-side compute must have a matching kernel in the vendored set, and in strict mode every vendored kernel must have a node.
  5. Chain validation. Named multi-step workflows are checked structurally, then a corpus run executes every fixture-backed one end to end and asserts run-to-run determinism.
  6. Vendor freshness. The site repository is checked out in CI and the committed vendored copies are compared against it semantically, so a stale catalog cannot deploy.
  7. Deploy, then post-deploy smoke: a real initialize against the live endpoint, followed by a live tool-count assertion against the committed count.
A green bundle does not prove the handshake works. Duplicate-name and other registration throws happen at server construction, which the bundler never executes. The only evidence that the deploy is healthy is an initialize returning 200 with a result from the live endpoint after the deploy. Treat the post-deploy smoke step as the deploy gate, and roll back on its failure rather than investigating with the endpoint broken.

A second gate worth copying if you run local pre-push checks: a parity check that fails CI when the CI job runs a gate the local pre-push script does not. Without it the two drift, and "green locally" stops predicting "green in CI".

7. Verify your own clone

The minimal call. Substitute your own endpoint:

curl -sS https://your-worker.example.com/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H 'mcp-protocol-version: 2025-06-18' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
        "protocolVersion":"2025-06-18",
        "capabilities":{},
        "clientInfo":{"name":"clone-check","version":"1.0.0"}}}'

A healthy server returns a JSON-RPC result carrying serverInfo and protocolVersion, possibly framed as a single server-sent event. Anything else, a 500, an HTML error page, or a stream that never produces a message, means the server threw during construction. Check for a duplicate tool name first.

Then confirm the catalog actually registered:

curl -sS https://your-worker.example.com/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'

Compare the returned count against the number your generation step recorded. A mismatch means the vendored data is stale, which is section 3 reasserting itself. Note that tools/list alone is a weak check: it proves registration, not that any tool runs. Call one real tool before believing a deploy.

The MCP Playground does all three steps against this deployment in a browser, with the raw request and response visible and a copyable client-config snippet, which makes it a reasonable reference for what the wire traffic should look like.

8. What to expect to get wrong first