What Unfenced is
Unfenced fetches any web page through a real browser on a real IP and hands your agent back clean, readable content instead of raw HTML - no headless-browser fleet to run, no anti-bot fighting, no HTML-to-text pipeline to maintain.
There are two ways to use it:
- Reading - give it a URL, get back markdown (or JSON, text, HTML). This covers most of what an agent needs: articles, listings, docs, feeds, and PDFs read as text.
- Live pages - for anything that needs a signed-in session, a click, or a form: open a real page, look at it, act on it, and read it back. The same engine can drive your own logged-in accounts, behind a permission model you control - it never acts on a site you have not allowed.
Reach both through one MCP connector (a small set of tools an agent calls directly), the zero-dependency TypeScript SDK, or the plain HTTP API the two share underneath.
Get an API key
Every connection to Unfenced - MCP, the SDK, or plain HTTP - authenticates with an API key.
- Sign in and open API keys in the dashboard.
- Create a key and name it after whatever will use it - an agent, a client, a script.
- The key is shown once. The same screen shows the exact setup for your platform - Claude, Cursor, VS Code, a CLI, a framework, or a config file - with that key already filled in.
Treat a key like a password: anyone holding it can act as your account, within whatever an attached agent is scoped to.
Connect an agent
Remote MCP - works today
Point any MCP-capable client at this URL, with your key as a bearer token:
https://unfenced.ai/api/mcp
Authorization: Bearer <your key>
The API keys screen in the dashboard has exact configuration for 31 clients and frameworks - Claude, Cursor, VS Code, Claude Code, Codex, LangChain, the Vercel AI SDK, plain HTTP, and more - with your key already spliced into every snippet. Copy from there rather than typing this by hand.
Local stdio bridge - once @unfenced-ai/mcp is published
A process on your machine that forwards every tool to this service over HTTP. It runs no browser of its own: the fetch still happens on our worker, on your account's exit IP and its stored logins, and is metered to your account. It exists for clients that cannot speak MCP over HTTP:
npx -y -p @unfenced-ai/mcp unfenced-mcp
This package is not on npm yet, so the command above does not work today. Use the remote MCP URL above in the meantime - it is the path that works right now, for every client that speaks MCP over HTTP.
TypeScript SDK - same "once published" caveat
A zero-dependency client (@unfenced-ai/sdk, Node 22+) that speaks the same HTTP
API, for building your own agent or script rather than wiring MCP:
import { Unfenced } from "@unfenced-ai/sdk";
const ac = new Unfenced({ baseUrl: "https://unfenced.ai/api", apiKey: process.env.UNFENCED_TOKEN });
const r = await ac.fetch("https://example.com");
if (r.outcome === "delivered") console.log(r.doc.markdown);
The same client drives live pages too - ac.open(url), then
observe, act, extract, close. Branch on
r.outcome ("delivered" · "offered" ·
"failed"); there is no r.ok.
Like the stdio bridge, @unfenced-ai/sdk is not on npm yet. The shape above is the
shipping client; until it publishes, the remote MCP URL is the path that works today.
The tools
Every tool your agent gets, grouped by what it does.
Reading a page
No live browser, no login, always available:
fetch_page- fetch one URL, get back clean markdown (or JSON, text, HTML).fetch_batch- fetch up to 20 URLs in one call.get_page_links- list a page's links, for crawling one level out without paying for the whole page.
A live page
Opens a real browser tab your agent can look at and act on, for logins, cookie banners, and anything that needs a click:
open_page- open a URL and keep it open so you can act on it.observe_page- re-read an open page, after a slow load or to find a control.act- click, type, select, scroll, drag, wait, and more - one action at a time.see_page- a screenshot of the page with every control outlined and numbered.extract_page- pull clean markdown or JSON out of a live page exactly as it stands - after logging in, after paginating, after expanding a section.fill_form- fill several fields in one call: a checkout, a booking, a sign-up.park_page- hold a page open while something else finishes: an emailed code, an approval in another app.read_download- read a file the page downloaded, including a PDF.close_page- release an open page.switch_account- switch a page to a different one of your sign-in accounts.whoami- confirm which account a page is actually signed in as.
Credentials and permissions
The guardrails around acting - see Permissions & credentials for how they fit together:
list_permissions- the sites your agent may act on.connect_site- a one-screen link to add a site, or a login, you are missing.list_credentials- the names of stored logins, never their values.pending_approvals- what is currently waiting on you to approve.
A small memory
remember, recall and forget hold short notes -
"invoice #4471 downloaded, waiting on approval" - that survive across sessions, so a later
session can pick up where an earlier one left off. It is for task state, never for secrets.
Permissions & credentials
Reading a page never needs permission. Acting on one - a click, a keystroke, a form submit - is gated by an allowlist your account owns, and an agent cannot add itself to it.
-
Reading is always allowed.
fetch_page,fetch_batch,get_page_linksand reading an open page work on any site, no grant required. -
Acting is granted per site. A site is either allowed (your
agent acts freely) or set to ask each time (every action waits for you to approve
it). A site on neither footing cannot be acted on.
list_permissionsshows the current list. -
An agent cannot grant itself a site. A key acts within an
account; it does not administer one. So the grant is a signed-in human decision: when your
agent reaches a site it may not act on, it gets a
connect_sitelink you open to add it, andpending_approvalsshows everything currently waiting on you.
Credentials never reach the agent or the model. A saved login lives in an encrypted vault the agent cannot read. It fills one by name, and the value is typed straight into the page:
actkind: "fill_secret"- a stored password, by credential name.actkind: "fill_totp"- the current 2FA code, derived from a stored seed.-
actkind: "fill_otp"- the one exception: a single-use code the user hands the agent in chat (an emailed or texted 2FA code, no stored seed). It is shape-checked, redacted in the log, and refused if it looks like a password.
list_credentials shows which names exist - never a value. There is deliberately
no tool that stores or reveals a secret; you add logins in the dashboard.
How failures come back
A failed call answers with a structured {"error": "<code>", "detail": "..."}
rather than just an HTTP status - the code is the instruction for what to do next, and the
codes are not interchangeable:
| Code | When | Means |
|---|---|---|
needs-credential |
401 | The page wants a signed-in session. No retry reaches it - fill a stored login. |
payment-required |
402 | The site quoted a price rather than refusing you. A purchasing decision, not an error. |
legally-restricted |
451 | Withheld for legal reasons, usually by jurisdiction. No tier or retry changes that. |
blocked |
403, 408, 429, 430, 503, or a challenge page | The site refused you, and a real browser already tried first. Try a different source. |
transient |
500, 502, 504 (and other 5xx) | The server faulted rather than refusing you. Worth retrying shortly. |
http-error |
404, 410 (and other 4xx) | The path is wrong or gone, and the site itself answered fine. Check the URL rather than retrying. |
A successful result can still carry contentConfidence: "low" with
contentWarnings - the text came back, but it may not be the page you asked
for, often a login wall. Worth a glance before you rely on it.
Live-page actions fail differently
A refusal from act, fill_form or open_page comes
back as {"ok": false, "code": "..."} - not an HTTP error - and the code says
what is missing:
| Code | Means | What to do |
|---|---|---|
permission-required |
The site is not on your act-allowlist. | Call connect_site; the owner allows it in the dashboard. |
credential-required |
A login field needs a stored secret. | act with kind: "fill_secret" (or fill_totp) by credential name. |
otp-required |
A one-time 2FA code is needed and no seed is stored. | Ask the user for the code, fill it with kind: "fill_otp". |
approval-required |
The site is "ask each time"; the owner must clear it. | It is queued in pending_approvals. |
confirmation-required |
The action would submit. | Retry the same action with confirm: true. |
Worked examples
Three flows end to end. The tool-call blocks show the arguments your agent sends; your MCP client handles the wire.
Read one page
The plain HTTP API - no client required. fetch_page is a thin wrapper over
exactly this:
curl -s -X POST https://unfenced.ai/api/jobs -H "Authorization: Bearer YOUR_KEY" -H "content-type: application/json" -d '{"url":"https://example.com","format":"markdown"}'
# -> 202 {"jobId": "..."}
curl -s https://unfenced.ai/api/jobs/JOB_ID -H "Authorization: Bearer YOUR_KEY"
# -> {"status": "done", "result": {"doc": {"markdown": "..."}}}
Poll the second call until status is "done", then read
result.doc.markdown.
Fetch a batch
One call, up to 20 URLs, sharing a 25,000-word budget; a URL that fails carries its own error in place, so one bad link never sinks the other nineteen:
{ "urls": ["https://a.example/1", "https://a.example/2"], "format": "markdown" }
Over the SDK: await ac.batch([url1, url2]) returns one result per URL, in
order.
Log in and act
Acting needs the site on your allowlist and, for a password field, a stored credential - whose value never passes through the agent:
1. open_page { "url": "https://app.example.com/login", "intent": "act" }
-> { "sessionId": "k3n9x2", "page": { ... } }
2. observe_page { "sessionId": "k3n9x2" }
-> controls, each with a ref: { "ref": "k3n9x2:e7", "name": "Email", ... }
3. act { "sessionId": "k3n9x2", "kind": "type", "ref": "k3n9x2:e7", "text": "me@example.com" }
act { "sessionId": "k3n9x2", "kind": "fill_secret", "ref": "k3n9x2:e9", "credential": "example-login" }
// the password is typed into the field - never through the agent
4. act { "sessionId": "k3n9x2", "kind": "click", "on": "Sign in", "confirm": true }
// confirm: true is required for anything that submits
5. act { "sessionId": "k3n9x2", "kind": "fill_totp", "ref": "k3n9x2:e12", "credential": "example-login" }
// only if a 2FA prompt appears; derived from the saved seed
6. extract_page { "sessionId": "k3n9x2", "format": "markdown" } // the signed-in page, clean
7. close_page { "sessionId": "k3n9x2" } // release the browser
A live page closes itself after a few minutes idle; park_page holds one open -
up to 30 minutes - while you wait on an emailed code or an approval elsewhere.