Skip to main content
Glama
dasecure

zapqr-mcp-starter

by dasecure

Auth for your MCP server in 5 minutes

An MCP server on Cloudflare Workers where Sign in with ZapQR is the authorization server. People connect Claude, Cursor or VS Code to your tools by signing in with a passkey or a scan from their phone. No passwords. No API keys to hand out. No user table to run.

npx degit dasecure/zapqr-mcp-starter my-mcp && cd my-mcp
npm install
npx wrangler deploy          # → https://my-mcp.<you>.workers.dev

Then register https://my-mcp.<you>.workers.dev/mcp at auth.zapqr.ai/account/agents and add that URL to Claude as a remote MCP server. That's the five minutes.


Why this works

The MCP authorization spec (2026-07-28) makes your MCP server an OAuth 2.1 resource server. It never signs anyone in. It does exactly three things, and this repo does all three in src/auth.ts:

#

Your server must…

Where

1

Publish RFC 9728 metadata naming its authorization server

GET /.well-known/oauth-protected-resource

2

Answer an unauthenticated request with 401 + WWW-Authenticate pointing at that metadata

challenge()

3

Verify each bearer token's signature, issuer, expiry and — the one that matters — that its aud is this server

verifyBearer()

Everything else happens between the MCP client and ZapQR: the client reads your metadata, finds https://auth.zapqr.ai, registers itself (RFC 7591, or by presenting a Client ID Metadata Document), runs the sign-in with PKCE and an RFC 8707 resource= naming your server, and comes back with a JWT. ZapQR shows the person a consent screen that reads "Connect Claude to your server (your-host)?"

Claude ──POST /mcp (no token)──▶ your Worker ──401 WWW-Authenticate: Bearer resource_metadata="…"──▶ Claude
Claude ──GET /.well-known/oauth-protected-resource──▶ { authorization_servers: ["https://auth.zapqr.ai"] }
Claude ──discovery, register, /auth?resource=<your URL>──▶ auth.zapqr.ai ──passkey / phone / consent──▶ person
Claude ◀── JWT { iss: auth.zapqr.ai, aud: <your URL>, sub, email, scope: "openid mcp:tools" } ──
Claude ──POST /mcp  Authorization: Bearer <JWT>──▶ your Worker ──verify against auth.zapqr.ai/jwks──▶ tools/call

Related MCP server: mcp-oauth-server-next

What's in the token

Every request to /mcp arrives with a verified identity on authInfo.extra.identity:

{ sub: 'a3f9…',                 // stable account id — key your data on this
  email: 'ana@example.com',     // verified by ZapQR when emailVerified is true
  emailVerified: true,
  scopes: ['openid', 'mcp:tools'],
  clientId: 'zq_…',             // which agent (Claude, Cursor, …) is calling
  expiresAt: 1790000000 }

Tokens last an hour; the client refreshes them without bothering the person.

The three strings

After you register on /account/agents you get exactly three strings. The Worker already knows two of them and derives the third from the request, so with the defaults there is nothing to configure. Set them in wrangler.toml only if you need to:

Var

Default

When to set

RESOURCE

<origin>/mcp of the incoming request

You serve the same Worker on several hostnames and registered one

ZAPQR_ISSUER

https://auth.zapqr.ai

You self-host the ZapQR IdP

scope

mcp:tools

Never — it's the one scope every registered server carries

The RESOURCE string must match what you registered, character for character. Trailing slash, port, path — all of it. It's an identifier, not a URL anyone fetches.

The tools it ships with

src/tools.ts registers five, and they are there to show the identity doing work, not to be kept:

Tool

What it proves

whoami

the verified identity made it through: sub, email (verified by ZapQR), scopes, which agent

echo

the tool round trip

remember / recall / forget

per-person isolation — notes live in Cloudflare KV under sub: and two people connecting the same server each see only their own. No login form, no API key, no user table; the isolation is a key prefix

remember/recall need a KV namespace: npx wrangler kv namespace create NOTES, paste the id into wrangler.toml. Without it the three tools answer with a clear error and everything else works.

Add your own

Each handler gets extra.authInfo.extra.identity — the same person on every call, verified on every request, no session to keep.

server.registerTool('list_orders', {
  description: 'Orders for the signed-in customer',
  inputSchema: { since: z.string().optional() },
}, async ({ since }, extra) => {
  const { sub } = extra.authInfo!.extra!.identity as Identity;
  const rows = await env.DB.prepare('select * from orders where customer = ?').bind(sub).all();
  return { content: [{ type: 'text', text: JSON.stringify(rows.results) }] };
});

Test it

npm test                 # 10 tests: token minted like ZapQR mints it → full MCP round trip + per-person isolation, no network
npm run check            # tsc
npx wrangler dev         # local, then add http://localhost:8787/mcp to Claude Desktop*

* Local dev works with a real ZapQR sign-in as long as you registered http://localhost:8787/mcp… which you can't — audiences must be https. For local work, mint a test token the way test/worker.test.ts does and pin the key with useKeys(), or deploy to a workers.dev preview and test there. Five minutes either way.

Connect it

Claude (web / desktop): Settings → Connectors → Add custom connector → paste https://my-mcp.<you>.workers.dev/mcp. Claude finds ZapQR from your metadata, registers itself and opens the sign-in.

Cursor / VS Code / Claude Code: add a remote server with that URL; each of these implements the same discovery.

Your own agent: any MCP client that implements the 2025-06-18 or later authorization flow. If yours only takes a URL and a static headers map, it can't run the flow — put an MCP-aware client in front of it.

See a finished one

ZapQR runs its own MCP server, built the same way this starter is: https://auth.zapqr.ai/mcp. Connect it (Claude → Settings → Connectors → Add custom connector) and you'll see the whole flow — discovery, sign-in, consent, a tool call — before you write a line.

It exposes your ZapQR account as tools an agent may use as you, behind two scopes:

Scope

Tools

zapqr:sessions — see and sign out your signed-in sessions

list_sessions, sign_out_session {id}, sign_out_everywhere_else {confirm: true}

zapqr:connections — see and disconnect the sites and agents connected to your account

list_connections, disconnect {id}

(none)

whoami

list_sessions marks the session that authorized the agent; sign_out_everywhere_else keeps that one and pushes a security notification. Disconnect the agent on your account page and its next call is refused — the same lever your users will have over the server you build here.

What this is not

  • Not an API key. A token is bound to one person, one agent, one server, and expires. If you want a long-lived machine credential, that's a different product.

  • Not a session. The Worker is stateless (sessionIdGenerator: undefined), so any isolate answers any request. Add a Durable Object if a tool needs conversation state.

  • Not a scope system. mcp:tools means "may use this server as me". Authorization inside your tools — who may see which order — is yours, keyed on sub.

Files

src/index.ts   routes: metadata, home page, the /mcp gate, CORS
src/auth.ts    RFC 9728 metadata · 401/403 challenge · JWT verification (jose) — the whole integration
src/tools.ts   your tools (whoami, echo, remember/recall/forget on KV)
test/          token minted like ZapQR mints it → 401 → verify → initialize → tools/list → tools/call
wrangler.toml  name, compatibility date, the two optional vars, the NOTES KV binding

Dependencies: @modelcontextprotocol/sdk (the protocol), jose (JWT + JWKS), zod (tool input schemas). Nothing else.

About ZapQR

ZapQR is a passwordless identity provider: passkeys first, scan-a-QR-approve-on-your-phone for screens that can't hold credentials, push-to-approve as the third path. It speaks OpenID Connect to websites, RFC 8628 device flow to hardware, and — with this starter — the MCP authorization spec to agents. Same account, same phone, every surface.

Guide with screenshots: zapqr.ai/mcp. Questions: vincent@dasecure.com.

MIT © DaSecure Solutions LLC

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    F
    maintenance
    A Model Context Protocol (MCP) server for authenticated REST APIs. Drop it into any project, point it at your API, and let AI agents call endpoints — with auto-login, 2FA support, Swagger spec fetch, and fuzzy endpoint search — all without writing a single line of auth code.
    3
    8 npm
    6
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    A proof-of-concept HTTP MCP server requiring OAuth 2.0 authentication before tool use. Demonstrates the full authentication flow with Claude Code, including discovery, client registration, PKCE authorization, and bearer token verification.
    -
  • A
    license
    Not graded
    quality
    A
    maintenance
    A spec-compliant remote MCP server with built-in OAuth 2.1 and Dynamic Client Registration, enabling Notion Custom Agents to connect via 'Sign in with OAuth' without bearer tokens. It supports SSO federation to Google and Microsoft Entra, and includes basic tools like whoami, echo, and slow_task.
    MIT