DEV Community

Cover image for I built a free QR code API + MCP server on Cloudflare Pages — here's the whole design
Zane Bin
Zane Bin

Posted on

I built a free QR code API + MCP server on Cloudflare Pages — here's the whole design

Most QR code generators are either ad-covered landing pages or SaaS products with a paywall behind "dynamic codes". I wanted the opposite: a generator that's free, private by default, and natively callable by AI agents. So I built FreeQrly — open source (BryceBin/freeqrly, MIT), running entirely on Cloudflare Pages + D1.

Here's the architecture, including the part I think is most interesting: one shared auth/quota core serving three surfaces — web UI, REST API, and MCP.

The three surfaces

1. Static QR API — no key, no signup, CORS wide open

GET https://freeqrly.com/api/qr?data=https://example.com&size=1024&color=2F4A62&bgcolor=ffffff&margin=4&ecc=M
Enter fullscreen mode Exit fullscreen mode

Returns an SVG. data is required (400 with a clear error otherwise), everything else has sane defaults, input is validated (data ≤ 2000 chars, color format checked). Static codes are generated 100% in the browser on the web UI — the API just mirrors that server-side for agents and scripts.

2. Dynamic codes — login-gated, editable after print

Dynamic codes resolve through short /q/xxxxxxx links, so you can change the destination after printing and see scan stats (date / country / device / city breakdowns). Free tier: 8 codes. Login via Google, GitHub, or email+password (Turnstile-protected). The /q/ hosted pages carry noindex + X-Robots-Tag so they never end up in search results.

3. MCP — Streamable HTTP, no auth for static, token for dynamic

POST https://freeqrly.com/mcp
Enter fullscreen mode Exit fullscreen mode

generate_qr works with zero auth. Send an Authorization: Bearer <token> header and five more tools appear in tools/list: dynamic_qr_create, dynamic_qr_list, dynamic_qr_update, dynamic_qr_delete, dynamic_qr_stats.

The design decision I'm happiest with

The API-token flow reuses the UI's auth and quota logic instead of duplicating it:

  • Users create tokens in the Dashboard ("API Tokens" section). Tokens start with fqly_, are shown exactly once, and only a SHA-256 hash is stored. Max 20 per account, revocable anytime.
  • All five dynamic-code REST endpoints share one module (functions/_lib/codes_api.js) with the UI and the MCP tools — same validation, same 8-code quota, same ownership checks. A token can't create or revoke other tokens (that requires a login session), and deleting your account wipes your tokens.
  • Quota violations return 403 quota_exceeded; cross-user access returns 403; revoked/unknown tokens return 401.

Because it's one code path, the three surfaces can't drift apart: what the UI enforces, the API and MCP enforce identically. I verified this end to end with a real test account in a real browser — login, token creation, REST create→list→stats→update→scan-redirect→delete, MCP tool calls, then 401s after revocation — and cleaned up all test data afterward.

Publishing to the MCP ecosystem

I added a server.json and published to the official MCP Registry as io.github.BryceBin/freeqrly (v1.1.0). Directories like Smithery and PulseMCP sync from the Registry, so one publish covers several. The repo also ships llms.txt and a skills/qr-generator/SKILL.md so agents can discover the API without reading docs.

What's deliberately boring

  • No framework on the frontend for the generator core — vanilla JS, so the page stays fast and the logic stays auditable.
  • SQLite via D1 for dynamic codes and token hashes. No ORM magic; the schema is tiny.
  • Analytics: Cloudflare Web Analytics for pageviews + Analytics Engine events (qr_download, batch_generate, scan_decode) that I query with plain SQL.

If you run an agent that needs QR codes — event badges, WiFi cards, vCards, batch generation (CSV in, ZIP out) — the static API and MCP are free with no signup: https://freeqrly.com/developers/ has the full reference. Happy to answer questions about the token/quota design in the comments.

Top comments (0)