camofox-mcp
# camofox-mcp
<!-- Badges -->
<p align="left">
<!-- License -->
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
<!-- Tech stack -->
<img src="https://img.shields.io/badge/TypeScript-5.0+-blue" />
<!-- MCP -->
<img src="https://badge.mcpx.dev?type=server" title="MCP Server"/>
</p>
A stdio [MCP](https://modelcontextprotocol.io) server that exposes the [camofox-browser](https://github.com/jo-inc/camofox-browser) HTTP API — an anti-detection browser automation server for AI agents — as MCP tools.
This is not a browser itself. It's a thin, generic bridge: every tool it registers is generated from `openapi.json`, the OpenAPI spec of the upstream camofox-browser server. Tabs, navigation, clicking/typing, accessibility snapshots, screenshots, sessions/cookies, and browser lifecycle are all covered simply by whatever operations exist in that spec.
On top of that bridge, this server adds security guardrails not present in the raw camofox-browser API: an SSRF guard that blocks tools from being pointed at internal infrastructure or cloud metadata endpoints, and a prompt-injection mitigation that wraps browsed content so the model treats it as data, not instructions. See the Security section below for details.
## 🔒 Security
This server drives a real browser on your behalf and feeds its output back to an LLM. That combination has two distinct attack surfaces, both of which this project mitigates by default — read this before deploying, and especially before setting `CAMOFOX_ALLOW_INTERNAL_URLS=1`.
### Prompt injection from browsed content
Any page the browser visits can contain text crafted to look like instructions to the model ("ignore previous instructions and...", fake system messages, hidden text, etc.). Every tool result returned from the camofox-browser server is wrapped with an explicit untrusted-content banner (see `UNTRUSTED_CONTENT_BANNER` in [`src/http.ts`](src/http.ts)) telling the model to treat the content strictly as data, never as instructions to follow.
This is a mitigation, not a guarantee — no banner can make an LLM fully immune to injection. Treat any agent using this server as able to act on adversarial content it browses, and scope its other tool access (file system, shell, credentials, other MCP servers) accordingly. Don't grant it access to secrets or destructive tools it doesn't need.
### SSRF (Server-Side Request Forgery)
Tools accept arbitrary URLs (e.g. "navigate to this page"), which an attacker — or a prompt-injected model — could point at internal infrastructure: your loopback interface, RFC1918 private ranges, link-local addresses, or cloud metadata endpoints like `169.254.169.254` (AWS/GCP/Azure instance metadata, often a path to credential theft).
Every field literally named `url` passed to a generated tool is checked by `assertPublicUrl` in [`src/security.ts`](src/security.ts) before any request is made:
- **Scheme allowlist** — only `http:` and `https:` are permitted; `file:`, `data:`, `gopher:`, etc. are rejected outright.
- **Blocked hostnames** — `localhost`, `localhost.localdomain`, and any hostname ending in `.local` or `.internal`.
- **Blocked IPv4 ranges** — loopback (`127.0.0.0/8`), private (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), link-local/cloud-metadata (`169.254.0.0/16`), carrier-grade NAT (`100.64.0.0/10`), and `0.0.0.0/8`.
- **Blocked IPv6 ranges** — loopback (`::1`), unique-local (`fc00::/7`), link-local (`fe80::/10`), and IPv4-mapped IPv6 addresses (`::ffff:0:0/96`) are unwrapped and checked against the IPv4 rules above.
- **DNS rebinding protection** — hostnames are resolved via DNS and *every* returned address is checked, so a public-looking hostname that resolves to (or is rebound to) an internal IP is still blocked, not just literal IP addresses in the URL.
**This guard is on by default and should stay on in any deployment with network access to sensitive internal services.** It can be disabled entirely by setting `CAMOFOX_ALLOW_INTERNAL_URLS=1` — only do this in an isolated/sandboxed environment (e.g. a container with no route to internal infrastructure or cloud metadata) where SSRF has no meaningful blast radius, such as local development against a camofox-browser instance on `localhost`.
### Credentials
`CAMOFOX_API_KEY` / `CAMOFOX_ACCESS_KEY` are sent as a bearer token to the configured `CAMOFOX_URL` on every request. Treat them as secrets: don't commit them, and don't point this server at a `CAMOFOX_URL` you don't trust, since the token will be sent to whatever host that is.
### Reporting a vulnerability
If you find a security issue in this bridge itself (not the upstream camofox-browser server), please open an issue or contact the maintainer directly rather than filing a public exploit.
## How it works
```
openapi.json --(npm run generate)--> src/tools/generated.ts --> src/index.ts registers MCP tools --> src/http.ts calls camofox-browser over HTTP
```
- `scripts/generate-tools.ts` reads `openapi.json` and emits `src/tools/generated.ts`: an array of operations, each with a name, description, HTTP method/path, field-to-location mapping, and a Zod schema derived from the JSON Schema.
- `src/index.ts` registers one MCP tool per generated operation on startup.
- `src/http.ts` (`callOperation`) is the runtime path every tool call goes through: it splits input into path/query/body per the field mapping, runs an SSRF check on any `url` field, sends the HTTP request to the camofox-browser server, and wraps the response as MCP tool content. Every successful result is prefixed with an untrusted-content banner instructing the model to treat webpage/browser-server content as data, not instructions.
- `src/security.ts` (`assertPublicUrl`) blocks non-http(s) schemes and any hostname/IP (including DNS-resolved) that is loopback, private, link-local, or reserved — this also covers cloud metadata addresses like `169.254.169.254`.
There is no per-endpoint or per-tool special-casing anywhere in the codebase. To add or change a tool, edit `openapi.json` and re-run `npm run generate` — never hand-edit `src/tools/generated.ts`.
## Requirements
- Node.js >= 18
- A running [camofox-browser](https://github.com/jo-inc/camofox-browser) server to connect to
## Usage
Run directly with npx — no install step needed:
```bash
npx @tonjun/camofox-mcp
```
### MCP client configuration
Point your MCP client (Claude Code, Claude Desktop, etc.) at the package via npx:
```json
{
"mcpServers": {
"camofox": {
"command": "npx",
"args": ["-y", "@tonjun/camofox-mcp"],
"env": {
"CAMOFOX_URL": "http://localhost:9377"
}
}
}
}
```
## Local development
```bash
npm install
npm run build # compile TypeScript to dist/
npm start # run the compiled server
npm run dev # run directly from source with tsx
```
## Configuration
Configured entirely via environment variables:
| Variable | Description |
| --- | --- |
| `CAMOFOX_URL` | Base URL of the camofox-browser server (default `http://localhost:9377`) |
| `CAMOFOX_API_KEY` / `CAMOFOX_ACCESS_KEY` | Bearer token sent as the `Authorization` header (`accessKey` takes precedence) |
| `CAMOFOX_ALLOW_INTERNAL_URLS` | Set to `1` to disable the SSRF guard in `src/security.ts` |
## Development
```bash
npm run generate # regenerate src/tools/generated.ts from openapi.json
npm run build # compile TypeScript to dist/
npm run dev # run the server directly from source with tsx
npm test # run the vitest suite
npm run test:watch # run vitest in watch mode
```
TDQS
Scored across 25 tools
Most tools target a distinct resource and action (create_tab, list_tabs, navigate, click, type, snapshot), and the descriptions are clear. The main ambiguities are health vs server_status vs metrics, and navigate auto-creating tabs which overlaps slightly with create_tab.
All tools share the camofox_ prefix and snake_case, with most following a verb_noun pattern like create_tab, list_traces, and import_cookies. Deviations like metrics, health, server_status, and bare verbs like click, type, and evaluate keep it from being perfectly uniform.
25 tools is at the heavy end of the scale, though most are justifiable for browser automation. The operational subset (metrics, health, server_status) and multiple trace/tab management tools could be consolidated, making the overall count feel larger than necessary.
The tool set covers browser lifecycle, tab creation/listing, navigation, interaction, snapshots, traces, and cookie import. A notable gap is the lack of a direct close/delete single tab by tabId—only close_tab_group, cleanup_idle_tabs, and destroy_session are available—plus no explicit tab activation tool.