Testing agent2web locally
The test suite and wrangler dev both run the real Worker locally, with local D1
and R2 — no Cloudflare account needed until you deploy. Everything except the Claude web
connector can be exercised this way, including the full
MCP
publish flow. Six steps, about five minutes.
You need: Node 22 or newer and this repository. Nothing else — no Cloudflare
account, no Docker, no DNS. Local D1 and R2 state lands in .wrangler/,
which is gitignored.
Install and run the test suite
The suite bundles the Worker exactly as wrangler does for deploy, boots it in
Miniflare with local D1 and R2 bindings, and talks to it over HTTP. A clean pass means the
deployed code path works, not merely that some in-process stand-in does.
Generate secrets for local dev
Save the admin password it prints — it is stored nowhere else.
.dev.vars is what wrangler dev reads for secrets, and it is
gitignored. The same variable names are what the Deploy to Cloudflare flow prompts for, so
the two stay in step.
.wrangler/; delete that directory to start clean.
Run the Worker
Bad configuration stops the Worker with the reason rather than half-starting. In another terminal:
Publish a page
This is the same call Claude makes. Export A2W_API_TOKEN from your
.dev.vars first.
Then curl -i localhost:8787/s/hello/. Note two headers: an
ETag straight from R2, so conditional requests work, and a sandbox
CSP,
because the page is served from the Worker's own origin.
accept header must list both application/json and
text/event-stream. That is the MCP spec, not a quirk of this server — omit
either and you get 406 Not Acceptable.
Password-protect it, and open the admin UI
Basic auth is throttled on the same counter as the form, because it issues no cookie and so pays the full key derivation on every request. Then sign in at localhost:8787/admin with the admin password from step 02.
Connect a real client
Both credentials work locally. The static token skips the browser; the OAuth flow uses a loopback callback, which the redirect policy allows on any port.
cloudflared tunnel --url http://localhost:8787 and set
A2W_PUBLIC_URL to the tunnel hostname — it must match exactly, since it is the
OAuth issuer and the audience of every token issued.
Troubleshooting
Every row here is a failure we actually hit while building this, with the symptom that showed up first.
| Symptom | Cause and fix |
|---|---|
| Worker returns 500 with a configuration message | Deliberate: bad config stops startup rather than half-working. The body names the
variable. A scrypt. password hash from the old self-hosted build lands here —
Cloudflare has no scrypt, so regenerate with npm run gen-secrets. |
406 Not Acceptable from /mcp |
The accept header must list both application/json and
text/event-stream. |
| Admin login says Incorrect credentials with the right password | Check .dev.vars is present and that the hash is intact. Hashes are
dot-separated precisely so an unquoted $ cannot be eaten by a shell. |
| Subdomain URLs 404 | A2W_SITES_BASE_DOMAIN must be a bare hostname, no port and no leading dot.
On workers.dev wildcards do not exist at all, so only path URLs work there. |
| A code change appears to do nothing | npm test and npm run dev both build first, but read the build
output — a failed tsc leaves the previous bundle in place and looks exactly like
a change that had no effect. |
| Port 8787 already in use | An earlier session is still alive: pkill -f wrangler, or pass
--port. If you change the port, change A2W_PUBLIC_URL with it. |
| Starting over | rm -rf .wrangler. Local D1 and R2 state is recreated on the next boot;
published sites and OAuth clients are gone. |
| Login is slow, or CPU cost looks high | Expected. PBKDF2 at 600,000 iterations is ~130 ms by design, which is why the Workers Paid plan is required — the Free plan caps a request at 10 ms of CPU. It is paid once per session, not per request. |