mcp-fetch-worker
by jkpe
README.md
# mcp-fetch-worker
A generic [MCP](https://modelcontextprotocol.io/) connector for bots and agents that need to call your self-hosted APIs without handing over long-lived credentials.
Put [Cloudflare Access](https://developers.cloudflare.com/cloudflare-one/access-controls/policies/) in front of this Worker with **Managed OAuth**, and bots like [Grok Bot](https://x.ai/bot) or [OpenClaw](https://openclaw.ai/) can connect via Dynamic Client Registration (DCR). You approve access when the bot first connects; revoke it any time from Access. The bot gets a scoped token tied to the connector, not a generic API key it can reuse elsewhere.
**Production MCP URL:** `https://<your-domain>/mcp`
## Why this exists
Managed bots are good at tool calling, but most homelab and self-hosted apps don't ship an MCP server. You could:
- **Use a service token**: persistent access via custom headers. Fine for scripts; risky for bots you don't fully trust.
- **Let the bot use Chrome**: it opens your web UI, you sign in through SSO, repeat. Works, but burns tokens and doesn't scale.
- **Build a bespoke MCP per app**: correct, but a lot of work for a Home Assistant API or a random internal dashboard.
This Worker is the middle ground: one thin MCP connector that exposes a single `http_fetch` tool. Point it at APIs on the same origin (already behind Access), and the bot can `GET`/`POST` them using the OAuth token you approved. No dedicated MCP server required on the other end.
See [First World Problem: How do I grant my bots access to my stuff without it wrecking everything?](https://www.jackpearce.co.uk/posts/granting-bots-access-without-wrecking-everything) for the full write-up.
## How it works
```
Bot (Grok Bot, OpenClaw, Cursor, …)
→ OAuth via Cloudflare Access Managed OAuth (you approve)
→ MCP at /mcp (this Worker)
→ http_fetch to allowed origins on your domain
→ Access JWT forwarded on outbound requests
```
1. **Access** handles authentication: Managed OAuth + DCR. The Worker is not an OAuth server; it validates the `Cf-Access-Jwt-Assertion` Access forwards.
2. **This Worker** serves Streamable HTTP MCP with one tool: `http_fetch`.
3. **Your APIs** sit on the same origin behind Access. Create **Linked App Token** policies so the forwarded JWT is accepted downstream.
Outbound fetches are restricted to the Worker's own origin by default (`ALLOWED_ORIGINS` overrides this).
## What it does
- Serves MCP at `/mcp` (and `/sse` as a legacy alias) using the [Agents SDK](https://developers.cloudflare.com/agents/) `createMcpHandler`
- Exposes one tool, `http_fetch`, for GET/POST requests against allowed origins
- Validates `Cf-Access-Jwt-Assertion` against your team's JWKS when `CF_ACCESS_TEAM_DOMAIN` and `CF_ACCESS_AUD` are set
- Forwards the caller's Access JWT on outbound fetches (`Cf-Access-Jwt-Assertion`, `Cf-Access-Token`, `Authorization: Bearer`)
## Deploy
```bash
npm install
npm run cf-typegen # regenerate Env types from wrangler.jsonc
npx wrangler deploy
```
Configuration lives in `wrangler.jsonc`. Workers Logs and Traces are enabled by default via the `observability` block.
### Custom domain route
Add a Worker route in `wrangler.jsonc` (or via the dashboard):
```jsonc
"routes": [
{
"pattern": "example.com/mcp*",
"zone_name": "example.com"
}
]
```
Then redeploy. The MCP endpoint is `https://<your-domain>/mcp`.
### Environment variables
Set in `wrangler.jsonc` `vars`, as [secrets](https://developers.cloudflare.com/workers/configuration/secrets/) (`wrangler secret put`), or in `.dev.vars` for local dev (see `.dev.vars.example`):
| Variable | Required | Description |
|----------|----------|-------------|
| `CF_ACCESS_TEAM_DOMAIN` | Production | `https://<team>.cloudflareaccess.com` |
| `CF_ACCESS_AUD` | Production | AUD tag from your Access application |
| `ALLOWED_ORIGINS` | No | Comma-separated fetch allowlist (default: Worker origin) |
| `MAX_RESPONSE_BYTES` | No | Truncate bodies above this size (default: 65536) |
| `DEFAULT_HEADERS` | No | JSON object of default outbound headers (secret; see below) |
When `CF_ACCESS_*` vars are unset, the Worker still serves MCP (useful for `wrangler dev`) but does not validate Access JWTs. **Set both vars in production.**
### Default headers for API keys
Store extra outbound headers as a Worker secret so `http_fetch` can call APIs that need a key without putting secrets in chat or baking one API into the Worker:
```bash
wrangler secret put DEFAULT_HEADERS
# paste: {"X-Api-Key":"your-key-here"}
```
For local dev, set a non-empty value in `wrangler.jsonc` `vars`, pass `--var DEFAULT_HEADERS='{"X-Api-Key":"..."}'` to `wrangler dev`, or use `wrangler secret put` on the deployed Worker. When `secrets.required` is configured, `.dev.vars` only loads those secret names, so put `DEFAULT_HEADERS` in `vars` or use `--var` locally.
`DEFAULT_HEADERS` must be a JSON object with string values, e.g. `{"X-Api-Key":"..."}`. On every `http_fetch` call, those headers are merged into the outbound request. Headers you pass in the tool call override defaults on name conflicts. Access JWT forwarding (`Cf-Access-Jwt-Assertion`, `Cf-Access-Token`, `Authorization`) always wins and cannot be overridden by defaults.
Unset or empty `DEFAULT_HEADERS` adds no extra headers.
## Cloudflare Access setup
1. Add a **self-hosted Access application** for your MCP URL (e.g. `https://<your-domain>/mcp` or a path covering `/mcp*`)
2. Enable **Managed OAuth** on the application (Advanced settings); this enables DCR for MCP clients
3. Copy the application's **AUD** tag into `CF_ACCESS_AUD`
4. Set `CF_ACCESS_TEAM_DOMAIN` to your team domain (e.g. `https://<team>.cloudflareaccess.com`)
5. Configure Access policies for who may connect: time-bound, email-based, or whatever fits your approval model
For downstream Access-protected APIs on the same origin, create **Linked App Token** policies on those applications so the forwarded JWT is accepted.
## MCP client configuration
Paste this URL into Cursor, Grok Bot, OpenClaw, or any Streamable HTTP MCP client:
```
https://<your-domain>/mcp
```
Use **Streamable HTTP** transport (not legacy SSE). The client will OAuth through Access Managed OAuth on first connect; that's your human-in-the-loop approval step.
## Local development
```bash
npm run dev
```
MCP is available at `http://localhost:8787/mcp` without Access validation.
## http_fetch tool
```json
{
"url": "/api/example",
"method": "GET"
}
```
- `url`: absolute URL or same-origin path
- `method`: `GET` (default) or `POST`
- `headers`: optional extra headers (override `DEFAULT_HEADERS` on conflict)
- `body`: optional POST body
`DEFAULT_HEADERS` (secret) are applied first; tool `headers` override them. Access JWT headers are always applied last.
Returns JSON with `status`, `headers`, `body`, and `truncated` flag.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues