MCP Server
MCP Server
Use FourA from any Model Context Protocol client (Claude Desktop, Claude Code, Cursor, Windsurf, VS Code) as four native tools and six workflow prompts. No integration code, no custom HTTP client.
Open source on GitHub; on npm as @fouradata/mcp. Current release: 0.8.1.
Quick Start: local stdio (recommended for Claude Desktop)
Grab a key at foura.ai/dashboard#api-keys (one click, shown once on creation, format pk_live_...). Drop this into your MCP client's config:
{
"mcpServers": {
"foura": {
"command": "npx",
"args": ["-y", "@fouradata/mcp"],
"env": { "FOURA_API_KEY": "pk_live_..." }
}
}
}
Claude Desktop gotcha: fully quit Claude Desktop (
Cmd+Qon macOS) before editing the config file. If the app is still running, it will overwrite your edits with its in-memory config on exit.
The npx command downloads @fouradata/mcp on first launch and runs it as a subprocess of your MCP client. No global install needed.
| Client | Where the config lives |
|---|---|
| Claude Desktop (macOS) | ~/Library/Application Support/Claude/claude_desktop_config.json |
| Claude Desktop (Windows) | %APPDATA%\Claude\claude_desktop_config.json |
| Claude Code | claude mcp add foura -- npx -y @fouradata/mcp (set FOURA_API_KEY in env first) |
| Cursor | ~/.cursor/mcp.json |
| Windsurf | ~/.codeium/windsurf/mcp_config.json |
| VS Code (MCP extension) | .vscode/mcp.json |
Restart the client. The tools (foura_auto, foura_single, foura_proxy, foura_browser) and six prompts appear in your tool list.
Quick Start: hosted (Streamable HTTP)
For clients that support the Streamable HTTP transport (Cursor, Windsurf, VS Code, Claude Code with --transport http), point them at the hosted endpoint instead of running a local subprocess:
{
"mcpServers": {
"foura": {
"url": "https://mcp.foura.ai/mcp",
"headers": {
"Authorization": "Bearer pk_live_..."
}
}
}
}
For Claude Desktop, use the stdio config above or bridge the hosted endpoint through mcp-remote:
{
"mcpServers": {
"foura": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://mcp.foura.ai/mcp", "--header", "Authorization: Bearer pk_live_..."]
}
}
}
Hosted endpoint reference
| Property | Value |
|---|---|
| URL | https://mcp.foura.ai/mcp |
| Transport | Streamable HTTP (POST /mcp, SSE responses) |
| Authentication | Authorization: Bearer pk_live_... per request |
| MCP-Protocol-Version | Per @modelcontextprotocol/sdk (currently 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05, 2024-10-07) |
| 401 challenge | WWW-Authenticate: Bearer realm="foura-mcp" |
The 401 challenge carries no RFC 9728 resource_metadata parameter, on purpose. Advertising one makes an OAuth-capable client start a flow this server doesn't implement. Send your pk_live_ key as a Bearer token and the 401 goes away.
The hosted server is stateless. Every request brings its own key, which the server forwards to the FourA API as X-API-Key. One key opens all four tools.
For protection against DNS-rebinding (CVE-2025-66414), the server validates the Host header (must be mcp.foura.ai or localhost) and the Origin header when present (allowlisted: mcp.foura.ai, claude.ai, app.cursor.sh, app.cursor.com). Server-to-server callers (curl, MCP clients in stdio bridge mode) send no Origin and pass through.
Tools
All four tools are annotated readOnlyHint: true and openWorldHint: true per the MCP 2025-06-18 spec. Clients that auto-approve trusted read-only tools call them without a per-request confirmation modal.
foura_auto is the smart default: give it a URL and it returns the content, picking the fetch method for you. The other three are the lower-level primitives it orchestrates; reach for them when you want explicit control.
foura_auto
Give it a URL when you want FourA to choose the request method. It makes bounded attempts across the available HTTP, proxy, and browser paths. Pass validate on protected targets so the response must contain content that identifies the real page. If no attempt satisfies the validation, the tool returns an error instead of presenting a challenge page as success.
The response includes completion details in meta and, by default, a reusable session with proxy, cookies, and userAgent. For a plain follow-up, call foura_single with session.proxy as proxy, serialize the cookies as a Cookie header, and send session.userAgent as the User-Agent header. For JavaScript rendering, pass the session values to the matching foura_browser fields.
Set exitClass: "premium" to let Auto escalate to a premium exit once the standard pool cannot reach the page; exitClass: "standard" keeps it off premium exits, and leaving the field out leaves the choice to Auto. It works as on foura_proxy: an allowance, not an instruction, counted against the premium traffic in your plan, and refused with code: "plan_limit_premium" on a plan without premium exits. The result's exitClass says which class delivered. See exitClass on Smart Fetch.
foura_single
One HTTP request, response back. Mirrors POST /api/single/ one-to-one.
Use for static pages, JSON APIs, server-rendered HTML.
Choosing which browser you present
A request presents the latest Google Chrome by default. When a target accepts one browser and refuses another, set browser (Chrome, Edge, Safari, Firefox, or Tor), os (Windows, macOS, Android, or iOS), or version, or pass an exact profile id:
{
"method": "GET",
"url": "https://example.com",
"browser": "Firefox",
"os": "Windows"
}
The newest version wins when several profiles match. A combination that does not exist returns an error listing what is available, so a request is never sent as a browser you did not choose. Selection needs unblocker, which is on by default. The catalogue is published at GET /api/profiles and needs no API key.
The same four fields sit inside foura_proxy's request object.
foura_proxy
Route one HTTP request through rotating proxies with automatic retry. Use it when foura_single is blocked or the target requires a specific exit country.
Set exitCountries to a strict allowlist of two-letter target-visible country codes supplied by the user or target requirements:
{
"maxTries": 5,
"exitCountries": ["CZ", "GB"],
"request": {
"method": "GET",
"url": "https://example.com/pricing",
"browser": "Chrome",
"os": "Windows"
}
}
Values are trimmed, uppercased, and deduplicated. Proxies with unknown exits are excluded, and the request never falls back to an unrequested country. Selection uses the latest available target-visible country metadata, normally updated within ten minutes; it is not a live geolocation lookup during the request. Do not infer the serving country from the proxy host address.
A scoped success returns exitCountry and the reusable proxy ID. Check that exitCountry belongs to the requested allowlist. If the current pool has no match, the tool returns code: "no_eligible_proxy" with the normalized scope in details.exitCountries. Preserve that scope and retry later. Change or widen it only when the user explicitly changes the requirement. Country scoping is included from the Startup plan up. On a plan without it, a call that sends exitCountries is refused with a 403 and X-FourA-Limit: plan_limit_feature.
If the selected page later needs JavaScript, pass the returned proxy ID to foura_browser.proxy so the browser reuses the same exit.
Set exitClass: "premium" for a target the standard pool cannot reach however many exits are tried. It is an allowance, not an instruction: the standard pool still races for the answer and usually wins, and a request it answers before any premium exit is tried costs no premium traffic. A premium attempt counts the traffic it carried even when it fails. The response reports exitClass back, premium or standard, so you can see per request which class served you. standard is also the answer once the premium traffic included in your plan is spent, and it is a normal result rather than an error. exitClass: "standard" forbids escalation outright. exitClass: "premium" on a plan without premium exits is refused with code: "plan_limit_premium". See exitClass.
When the rotation had to move to another browser to get an answer, a successful response carries profile: the catalogue profile id it settled on. Pass it as profile on the next call to present the same browser.
A failed rotation carries attemptReport beside the error: one summary sentence, plus counts that separate exits that never answered (noResponse), exits a bot check refused (defense, with the vendors in vendors), pages that arrived and were rejected only by your own validate.data (contentRejected), statusRejected, and other. profilesTried lists the browsers the task sent, in first-use order, with default meaning the request went out exactly as written. A high contentRejected means FourA delivered real pages and your own rule threw them away. See Why a Proxy Request Ran Out of Tries.
foura_browser
Full browser session. JavaScript runs, the DOM renders, cookies come back. Mirrors POST /api/browser/.
Use for single-page apps, lazy-loaded content, or pages with a check that needs a real browser to complete.
For input shapes, defaults, and validation rules on each tool, refer to the REST endpoint reference. Tool schemas match the REST API field-for-field, plus the MCP-only offload_large opt-in (see below).
When a target runs a bot check
foura_single and foura_proxy return defense when the target ran a bot check on the way to the body. defense.solved: true means the check was met and data is the real page; false means the body may be a challenge page. Retry with a different browser, os, or version, or move up to foura_proxy or foura_browser, instead of treating the challenge page as content.
Typed responses
Every tool response includes both content (human-readable text summary) and structuredContent (typed JSON validated against the tool's outputSchema). Each tool has its own unique shape:
foura_auto: single-shaped{ status, headers, data }plusmeta({ rung, solved, attempts, credits }, always present, whererungis one ofcache,probe,proxy,browser,warmup,fail) and, by default,session({ proxy, cookies, userAgent }) for replay through the lower-level tools; a request that named a class also includesexitClass. Nototal_time.foura_single:{ status, headers, data, total_time, ... }(headers is an array, one entry per redirect hop)foura_proxy: same as single plus{ proxy, total }; a scoped success also includesexitCountry, a request that named a class includesexitClass, a rotation that changed browser family includesprofile, and a failure includesattemptReportfoura_browser: distinct shape{ status, headers: object, body, cookies, userAgent }(note:bodymay be a string or object depending on content-type)
Every tool also reports what the call cost and how to trace it, read from the API's response headers:
credits- credits this call spent. Present on failures too, because the work was done either way. You're billed only for a successful call, so a failure shows its credits here but costs you nothing.request_id- FourA's id for the call. Quote it in a support request.exitClass-premiumwhen a premium exit served the call. Onfoura_singleandfoura_browserthat happens whenproxyreplays a premium exit thatfoura_proxyorfoura_autofound.
Each is absent when the API reported nothing, so a client written against an earlier version keeps working unchanged. The same values are documented at Response Headers.
Clients that support structuredContent can pass the typed object directly to the LLM instead of requiring it to parse JSON from prose.
Multi-value response headers
Headers that appear multiple times (Set-Cookie, Link, WWW-Authenticate) come back as arrays:
{
"headers": [
{
"result": { "version": "HTTP/2", "code": 200, "reason": "" },
"content-type": "text/html",
"set-cookie": ["a=1; Path=/", "b=2; Path=/"]
}
]
}
This matters for sites that set session + tracking + consent cookies in one response (most of e-commerce).
Large responses: offload_large (default: inline)
By default (since v0.2.0), full response bodies are returned inline in structuredContent regardless of size. This works in every MCP client out of the box.
If your client supports MCP resources/read AND you want token savings on big pages, pass offload_large: true per tool call. Responses >= 50 KB are then written to disk, returned as a resource_link, and your client fetches the body only when it actually needs it. On the hosted server, cached payloads expire after 1 hour. On your own instance nothing deletes stored payloads: clear files older than an hour from the payload directory yourself.
{
"method": "GET",
"url": "https://en.wikipedia.org/wiki/Web_scraping",
"offload_large": true
}
| Client | offload_large: true |
|---|---|
| Claude Desktop | not yet, leave default false |
| Claude Code, Cursor, Windsurf | supported |
| VS Code MCP extension | supported |
Tenant-isolated: each API key gets its own namespace (sha256(apiKey)[:16]). Only the key that stored a payload can read it back. Cross-tenant reads return Payload not found with no existence leak.
Built-in Prompts
Six workflow templates surface under /prompts in any MCP client. Each takes named arguments and returns a templated user message orchestrating one or more tools.
| Prompt | Arguments | What it does |
|---|---|---|
smart_fetch |
url, optional must_contain, extract |
Auto fetch (picks the method, handles bot protection), then return or extract the content |
scrape_product_page |
url |
Browser fetch, then extract product title, price, image, stock, SKU as JSON |
extract_article |
url |
Single with proxy fallback, then strip nav/ads and return clean article JSON |
monitor_pricing |
url, optional target_price |
Proxy fetch, extract current price, compare to target |
check_endpoint_health |
url, optional expected_text |
Single with strict validation, return reachability and timing |
bulk_fetch_urls |
urls (comma-separated) |
Parallel single, auto-fallback to proxy per URL, return metadata only |
Prompts cost zero tokens at idle. Only invoked prompts enter the LLM context.
Full text plus manual fallback prompts: MCP Recipes.
Error envelope
Every error (isError: true) carries a structuredContent envelope. Minimum fields on every error:
{
"service": "auto | single | proxy | browser",
"code": "rate_limited",
"error": "Rate limit exceeded"
}
On upstream errors with HTTP status, status is also present. On rate-limit and capacity errors, the upstream envelope adds retryAfter, current.{concurrency, rpm}, and limits.{maxConcurrency, maxRpm}. See API Errors for the underlying REST shape.
Stable code values:
| Code | HTTP | Meaning | Retry safe? |
|---|---|---|---|
ssrf_blocked |
n/a | The target is a private or reserved address (RFC 5735, 6598, IPv6 reserved), the URL isn't http(s), or its host name didn't resolve | No, check the URL. A lookup that failed briefly can be retried |
upstream_non_json |
varies | Upstream returned malformed body | Maybe, investigate |
output_validation_failed |
n/a | The MCP server's outputSchema rejected the upstream response, or the tool couldn't complete the call at all (no API key configured, API unreachable) |
Maybe: check the setup, then report |
bad_request |
400 | Input shape rejected, or the target URL points at FourA's own API instead of a page (only /inspect can be fetched there) |
No, fix arguments |
auth_failed |
401 | Key missing, invalid, or deactivated | No, fix the key |
forbidden |
403 | The target answered 403 and your validate rejected it (a site check, a country restriction) |
No, or switch to foura_proxy |
not_found |
404 | Target or endpoint missing | No |
rate_limited |
429 | RPM cap hit | Yes, wait retryAfter |
at_capacity |
503 | Concurrency cap hit | Yes, wait retryAfter |
service_disabled |
503 | The service is switched off for maintenance. A tool your plan doesn't include comes back as plan_limit_feature |
Contact support |
service_unavailable |
503 | Generic 503 | Yes, short backoff |
upstream_error |
500+ or 0 | The target answered with a server error, or on foura_proxy, foura_browser and foura_auto never answered |
Yes, exponential backoff |
upstream_client_error |
4xx | Other 4xx | Usually no |
upstream_unknown |
other | The request ran but produced no accepted answer: on foura_single the target never answered (timeout, refused connection), and on any tool your validate rejected a 2xx or 3xx response. Read status and error |
Investigate |
no_eligible_proxy |
n/a | No proxy matches the strict exitCountries scope |
Retry later; change scope only explicitly |
plan_limit_* |
403 or 429 | One of your plan's limits refused the call: plan_limit_ followed by feature, premium, concurrency, rate, browser_daily, credits or bandwidth. See MCP Server Errors |
Wait retryAfter when present; otherwise not until the limit resets or the plan changes |
LLM agents can read code directly for retry logic without parsing prose. Authentication walkthrough: Authentication.
Limits
- Inline body by default. With
offload_large: true, responses >= 50 KB go to disk +resource_link(per-tenant, 1-hour TTL). - Private targets are refused (RFC 5735, RFC 6598, IPv6 reserved blocks) at the MCP layer. Only public hosts forwarded.
- Request body cap of 256 KB on incoming
/mcprequests (real MCP payloads are < 4 KB). - Rate limits are enforced by the FourA API per service. See Rate Limits.
Self-Hosting
The full server source is public on GitHub under @fouradata/mcp. Clone the repo, npm install, npm run build, and run node dist/http.js to stand up your own instance. Runs stateless in a single container behind any load balancer.
Configurable environment:
| Variable | Default | Purpose |
|---|---|---|
PORT |
3076 |
HTTP listen port |
FOURA_API_BASE |
https://api.foura.ai/api |
Upstream FourA REST base URL |
FOURA_MCP_PAYLOADS_DIR |
a foura-mcp-payloads folder in the system temp directory (the bundled Docker Compose file sets /data/payloads) |
Where >= 50 KB responses are cached on disk (with offload_large: true) |
FOURA_MCP_ALLOWED_HOSTS |
mcp.foura.ai,localhost,127.0.0.1,[::1] |
Hostname allowlist for Host header (DNS-rebinding defense) |
FOURA_MCP_ALLOWED_ORIGINS |
https://mcp.foura.ai,https://claude.ai,https://app.cursor.sh,https://app.cursor.com |
Origin allowlist for browser callers |
The official container runs as uid 1001 (non-root). The /data/payloads host bind mount must be writable by that uid.
Scale horizontally behind any load balancer. Clients supply their key on every request, so there's no sticky session.