Perxel MCP Kit
OfficialClick on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Perxel MCP Kitshow me the dashboard metrics for my MCP over the last 7 days"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Perxel MCP Kit
A public template for building a remote MCP server plus its monitoring
dashboard, 100% on Cloudflare Workers. Every MCP built from it follows the
same checklist: see STANDARD.md.
Source-available under the PolyForm Shield 1.0.0 license (LICENSE): free
to use and modify, not to resell or offer as a competing service. This is
not open source (OSI).
v1 is tested and supported for Claude only (claude.ai web, Desktop, mobile, Claude Code).
Infrastructure
Read this first. It answers "how many domains, Workers, and what runs where".
At a glance
Per Cloudflare account: one subdomain and one Worker per MCP, plus one shared metrics-proxy Worker (no public URL). No containers, no Pages, no separate dashboard host, no hosted Inspector.
┌──────────── Cloudflare account ─────────────────────────────┐
Claude ──/mcp────────▶ │ MCP Worker "<slug>-mcp" https://<host> │
│ /mcp / /health OAuth routes │
You (browser) ─/dash─▶ │ /dash ◀─ Access app (edge sign-in) + JWT re-check │
via Access │ │ service binding METRICS_PROXY (Worker-to-Worker) │
│ ▼ │
│ Metrics proxy Worker "mcp-metrics-proxy" (no URL) │
│ │ holds the only Analytics Engine read token │
│ ▼ │
│ Analytics Engine dataset mcp_<slug> (written by /mcp) │
│ KV: OAUTH_KV, CONTENT_KV · Access: sign-in app, /dash app │
└─────────────────────────────────────────────────────────────┘
Your laptop: MCP Inspector (`pnpm dev` / `pnpm inspect`), never deployed.Hostnames
Hostname | Worker | Notes |
| the MCP Worker | Attached as a Workers Custom Domain from |
That's the only hostname. workers_dev and preview_urls are off on both
Workers, so nothing is reachable any other way.
Paths on <host>
Path | What | Who can reach it |
| The MCP endpoint ( | Public tools: anyone. Private tools: OAuth (Access one-time PIN sign-in, then consent). |
| Docs page generated from the tool list, with connect steps. | Public |
|
| Public |
| OAuth 2.1 (discovery, DCR/CIMD, PKCE, refresh), sign-in via Cloudflare Access for SaaS (OIDC). | Public (it's the OAuth flow) |
| Monitoring dashboard: calls by status, latency p50/p95, per-tool table, clients, auth split, countries, top problems; 24h / 7d / 30d / 90d. Server-rendered HTML + SVG, no client JS. | Access-protected: a Cloudflare Access app on |
DASH_PATH is configurable (/dash default, any path like /ops/metrics,
or off). It can't be / or shadow an MCP route. If you change it, change
the Access app's path to match.
Workers
Worker | Config | Count | What it holds |
|
| one per MCP | The MCP, OAuth, docs, |
|
| one per Cloudflare account, shared | The only Analytics Engine read token ( |
Why a separate proxy at all: an Analytics Engine token can't be scoped to one
dataset. If every MCP Worker held it, one compromised MCP could read every
other MCP's metrics. With the proxy, an MCP Worker holds only its own key, which
can only read mcp_<slug>.
Cloudflare Access (Zero Trust)
Access app | Type | Count | Used for |
MCP sign-in | SaaS, OIDC. Redirect URI | one per MCP | Signing in users of private tools. Gives the five |
Dashboard | Self-hosted, domain | one per MCP (none when | Who can open |
Seats: sign-in users and dashboard viewers share Zero Trust's 50 free users across all MCPs in the account, then about $7/user/month.
What is deliberately not deployed
MCP Inspector. It's a Node (Express) app, not a static site: its browser UI talks to its own backend (
/api/mcp/connect,/api/mcp/send, and an open/api/fetchproxy), so Pages can't host it. Workers can't run it either, and it hardcodes root paths (/assets,/api), so it can't sit under/inspector. Hosting it would take a container plus an Access app, just to expose a generic fetch proxy. Run it locally instead:pnpm dev(against local) orpnpm inspect https://<host>/mcp --allow-remote(against production, on purpose).Grafana. Earlier versions ran Grafana in a Cloudflare Container on a separate dashboard host: one more Worker and a container per MCP, cold starts, and a Docker build. A fixed set of panels covers "is this MCP healthy and used", so the dashboard is now part of the MCP Worker. If you really want ad-hoc querying, Grafana is an add-on pointed at the same proxy (give the proxy a URL only for that case).
Cloudflare Pages. Nothing here is a static site. Cloudflare also steers new projects to Workers (with static assets) rather than Pages.
What wrangler does and doesn't do
Wrangler CLI | Cloudflare dashboard (or API with a token) |
Create KV namespaces, set secrets, deploy both Workers, attach the custom domain ( | Access apps and the Analytics Engine read token (both done by |
Costs (checked 2026-09-30)
Workers Paid, $5/month per account: covers every MCP and the proxy at small scale (10M requests, 30M CPU-ms included).
Zero Trust: 50 free seats shared, then about $7/user/month.
No container cost.
LLM tokens are paid by the user's Claude subscription, not by the MCP.
Related MCP server: Cloudflare Remote MCP Server (Authless)
What's inside
STANDARD.md SSOT checklist: protocol, tools, auth, limits, metrics row, monitoring, tests, costs
kit/ owned by the kit; clones never edit (updated via pnpm kit:update)
core/ gate, OAuth + Access sign-in, consent page, rate limit, metrics, logs, health,
docs page, dashboard (dash.ts), static JSON source + search
metrics-proxy/ shared Worker holding the only Analytics Engine token; scopes each key to one dataset
testing/ protocol test, gate tests, eval runner, inspect launcher
update/ the kit:update script
setup/ the setup:cloudflare script (Cloudflare account setup from one global API key)
src/tools/ small generic example (one public, one private tool)
mcp.config.ts example config
evals/ example questions
wrangler.mcp.jsonc the MCP Worker (one per MCP)
wrangler.proxy.jsonc the metrics proxy (one per account)Create a new MCP from the template
Use the template. On GitHub: Use this template → create your repo (e.g.
docs-mcp,shop-mcp), then clone it.Install.
pnpm install(Node 22, pnpm 10.22.0).Set
mcp.config.ts.slug([a-z0-9-]; gives the datasetmcp_<slug>with-→_),name,version,description,scopes(every private tool's scope must be listed), and thetoolslist.Set
wrangler.mcp.jsonc.name(<slug>-mcp), theroutespattern andMCP_PUBLIC_URL(same host, e.g.mcp.example.comandhttps://mcp.example.com), the dataset name, unique rate-limiternamespace_ids, andCONTENT_URLif you use a static source. Copy.dev.vars.exampleto.dev.varsfor local dev.Write tools. Add files in
src/tools/withdefineToolfromkit/core/tool.ts:name(snake_case),title,description(what it returns, units, freshness, limits),access: "public" | "private"(plus ascopewhen private), a zodinputSchema, andexecute(input, ctx). Keep the token rules fromSTANDARD.md: list/search tools return compact items only (title, slug, excerpt, URL, category, never full text), full text comes only from a detail tool for one item, every list has alimit(default 5, max 20), no "dump everything" tool. Errors: thrownew ToolError("not_found" | "bad_input" | "upstream" | "unavailable", msg); anything else becomes"Internal error".Tests.
pnpm typecheck && pnpm test: unit tests per tool, the protocol test (runs against your own config), gate tests (200/401/403), dashboard and proxy tests.Evals. Edit
evals/questions.md(one##question:- tool:lines with JSON args,- expect:lines with facts that must appear), thenpnpm eval --url http://localhost:8788/mcpagainstpnpm dev.Clones never edit
kit/. Kit fixes arrive viapnpm kit:update.
Content source (static JSON)
For a site-backed MCP (e.g. an MCP over your own website), the site publishes one JSON file at build time and the MCP reads it:
The file must be at a public URL (e.g.
https://example.com/content.json), set asCONTENT_URL. The Worker has no filesystem, so a file in the repo isn't read at runtime.Recommended shape and search rules:
STANDARD.md→ "Static JSON source".In
src/, create the source once at module level and pass it to both the tools and/health:
// src/content.ts
import { z } from "zod";
import { createStaticSource } from "../kit/core/static-source.js";
export const content = createStaticSource({
name: "content",
url: (env) => env.CONTENT_URL as string,
kvKey: "content",
parse: (json) => Content.parse(json), // your zod schema
});
// src/index.ts
export default createWorker(config, { sources: { content } });Loading order: fresh memory (about 10 min) → conditional fetch (
If-None-Match) → stale memory →CONTENT_KVlast-good copy → a clear "unavailable" error./healthloads the source too, so it reports what a tool call would get (memory,fetch,kv-fallback, orempty→ 503).Content changes show up within about 10 minutes of the site deploy, with no MCP redeploy.
Local development
Command | What |
|
|
| The Inspector alone, against local by default. |
| Eval questions; also refuses non-local URLs without |
| Must pass after every change. |
The local dashboard skips the Access check (MCP_PUBLIC_URL is localhost in
.dev.vars). It shows an empty page with a notice unless the proxy runs too:
wrangler dev -c wrangler.mcp.jsonc -c wrangler.proxy.jsonc with
DASH_PROXY_KEY, CF_API_TOKEN and PROXY_KEYS in .dev.vars (that queries
the real Analytics Engine, so use a real read token only when you need to).
Deploy
Never deploy, secret put, or create resources from a dev machine without
the owner's approval (STANDARD.md → Dev rules).
One command: pnpm setup:cloudflare
The only input is the account's Global API Key (Cloudflare → My Profile →
API Tokens → Global API Key) and the login email, in .dev.vars (gitignored)
or the environment:
CF_EMAIL="you@example.com"
CF_GLOBAL_API_KEY="..."Set name, routes, MCP_PUBLIC_URL and the mcp_<slug> dataset in
wrangler.mcp.jsonc, then:
pnpm setup:cloudflare --dry-run # reads only, prints what it would do
pnpm setup:cloudflare # does it, deploys, checks /healthFlags: --emails a@x.com,b@x.com (who may sign in and see the dashboard;
default: the login email), --policy "<name>" (reuse an existing reusable Access
policy by name; default MCP access), --no-deploy.
It is idempotent (looks first, creates only what is missing, safe to re-run) and
drives wrangler with the same key, so no wrangler login is needed. It does:
Step | What |
Account | Finds the account; fills |
Zero Trust | Reads the team domain into |
Access policy | Creates (or reuses) one reusable allow policy for the emails |
Access apps | Creates the SaaS OIDC sign-in app ( |
KV | Creates |
Rate limits | Picks |
Metrics proxy | Deploys |
Secrets |
|
Proxy keys | Keeps every MCP's key hash in a |
Deploy |
|
One thing it cannot do: enable Zero Trust on an account for the first time
(one click, Zero Trust → Get started, free plan). It stops with that message
if it is off. Also: Cloudflare shows a SaaS app's client secret only when the
app is created, so if ACCESS_CLIENT_SECRET is ever lost, delete the sign-in
Access app and re-run.
The global key is all-powerful: keep it in .dev.vars only, never commit it,
and rotate it if it leaks. The Worker never reads it.
Manual steps (what the script automates)
Only needed to understand or repair a setup by hand. Run pnpm exec wrangler login first (it opens a browser).
Once per Cloudflare account (the first MCP does this)
Workers Paid plan ($5/month). Don't run production on free (its 10 ms CPU cap per request is a real risk).
Analytics Engine read token (dashboard → My Profile → API Tokens → Create: Account → Account Analytics → Read, this account only).
Metrics proxy. Set
CF_ACCOUNT_IDinwrangler.proxy.jsonc, then:pnpm exec wrangler secret put -c wrangler.proxy.jsonc CF_API_TOKEN pnpm exec wrangler secret put -c wrangler.proxy.jsonc PROXY_KEYS # full JSON map, see below pnpm exec wrangler deploy -c wrangler.proxy.jsoncZero Trust team domain (Zero Trust → Settings → team name,
<team>.cloudflareaccess.com) goes in every MCP'sDASH_ACCESS_TEAM.
Per MCP
KV.
pnpm exec wrangler kv namespace create OAUTH_KVand... CONTENT_KV; paste the ids intowrangler.mcp.jsonc.Access sign-in app (Zero Trust → Access → Applications → Add → SaaS → OIDC). Redirect URI
https://<host>/callback; add a policy (e.g. email one-time PIN for allowed emails). Note the client ID, client secret, token URL, authorization URL and JWKS URL.Access dashboard app (Add → Self-hosted). Domain
<host>, pathdash; policy: the emails allowed to see metrics. Copy its Application Audience (AUD) tag intoDASH_ACCESS_AUD.Proxy key. Generate one (
openssl rand -hex 32), hash it (printf %s '<key>' | shasum -a 256), add"<hash>": "mcp_<slug>"to the proxy'sPROXY_KEYSand re-put the whole map (a secret put replaces the value), then set the key on the MCP:pnpm exec wrangler secret put -c wrangler.mcp.jsonc DASH_PROXY_KEY.MCP secrets (
pnpm exec wrangler secret put -c wrangler.mcp.jsonc <NAME>):IP_HASH_SALT,COOKIE_ENCRYPTION_KEY(random,openssl rand -hex 32),ACCESS_CLIENT_ID,ACCESS_CLIENT_SECRET,ACCESS_TOKEN_URL,ACCESS_AUTHORIZATION_URL,ACCESS_JWKS_URL,DASH_PROXY_KEY.Deploy.
pnpm exec wrangler deploy -c wrangler.mcp.jsonc. The custom domain, DNS record and certificate are created here.Check.
https://<host>/healthisok: true(and the source showsfetch).https://<host>/lists the tools.https://<host>/dashasks for Access sign-in, then shows panels (empty until the first tool calls).In Claude: Settings → Connectors → Add custom connector →
https://<host>/mcp. Public tools work at once; a private tool shows a Connect card → Access PIN → Allow → Claude retries.pnpm exec wrangler tail <slug>-mcpfor live logs.
Where every value comes from
Name | Kind | Where it's set | Source |
| var |
|
|
| var |
| the site's public JSON URL (only with a static source) |
| var |
|
|
| var |
| Zero Trust team domain (per account) |
| var |
| AUD tag of the |
| var |
| Optional. |
| secret | MCP | random |
| secret | MCP | the Access SaaS/OIDC app |
| secret | MCP | random; its hash goes in |
| var |
| dashboard account id |
| secret | proxy | Account Analytics Read token |
| secret | proxy |
|
Public dashboard demo
By default /dash is private (Access, above). To show it off as a live demo
with no sign-in, opt in per MCP:
wrangler.mcp.jsoncvars:"DASH_PUBLIC": "true"(any other value keeps it private).Delete the dashboard Access app on
<host>/dash(Zero Trust → Access → Applications), or/dashstill asks for a login.setup:cloudflareskips creating it and warns if it exists.wrangler deploy -c wrangler.mcp.jsonc.
Everything on the dashboard becomes world-readable (call counts, clients,
countries, tool names, problems), so only do this for an MCP whose usage you
are happy to show. The sign-in app for private tools is unaffected. To go
private again, remove the var and re-run pnpm setup:cloudflare.
kit:update
Pulls the latest kit/ from the kit repo without touching the clone's own
files (src/, mcp.config.ts, evals/, wrangler configs, package.json):
pnpm kit:update [ref] [--from <local path>]Refuses if
git status --porcelain kit/is not empty (commit first).Default source is
kit.json(repo+ref, tarball from GitHub);--fromcopies from a local checkout.Replaces
kit/entirely, writeskit/VERSIONas<ref>@<short sha>, compareskit/deps.jsonagainst the clone'spackage.jsonand prints any mismatches with thepnpm addcommand to fix them (never editspackage.jsonautomatically), then printsgit diff --stat kit/.Review the diff, run
pnpm typecheck && pnpm test, commit.Clone-owned files the kit changed in a release (wrangler configs,
package.jsonscripts) are listed in that release's commit message; copy those changes by hand.
Transferring to another Cloudflare account
See TRANSFER.md.
This server cannot be deployed
Maintenance
Related MCP Connectors
Hosted MCP server with managed OAuth for 15+ toolkits: Google Workspace, Fitbit, Oura, Kalshi, etc.
Hosted remote MCP server for YNAB on Cloudflare Workers with OAuth
Cloudflare Workers MCP server: api-landing
MCP server for AI agents to plan, verify, and deploy Cloudflare-native apps.
Related MCP Servers
- FlicenseNot gradedqualityNot gradedmaintenanceA template for deploying MCP servers on Cloudflare Workers without authentication. Provides a foundation for building custom MCP tools that can be accessed remotely from various MCP clients like Claude Desktop or the Cloudflare AI Playground.-
- FlicenseNot gradedqualityDmaintenanceA template for deploying custom MCP servers on Cloudflare Workers without authentication. Allows you to create and expose custom tools that can be accessed from remote MCP clients like Claude Desktop or Cloudflare AI Playground.-
- FlicenseNot gradedqualityCmaintenanceA Cloudflare Workers-based MCP server template for building remote MCP servers without authentication. Provides a starting point for deploying custom tools accessible from Claude Desktop or other MCP clients.-
- FlicenseNot gradedqualityDmaintenanceA production-ready template for building MCP servers on Cloudflare Workers that expose server-side tools with rich, interactive React-based UI widgets using the MCP Extensions Apps API.12-