Oxylabs Web API MCP Server
This server gives MCP agents live web access through the Oxylabs Web API — search, read, and extract from web pages, all read-only.
search— live web search returning ranked organic results (title, description, URL), with geo-location and result-count control.scrape— fetch any http(s) URL as Markdown (or HTML), including JavaScript-heavy and bot-protected pages; large pages are offloaded to disk or truncated, andrun_jsreturns a job id for slow renders.extract— pull named fields off a page as JSON without writing selectors (per-run user approval required by default).check_scrape— poll background JavaScript-rendering jobs started byscrapeorextract, returning the finished result.read_scraped— read an offloaded page in chunks via offset andnext_offset, stopping when you have what you need.list_scrapers/scrape_target— discover the API's target-specific scrape endpoints and call them with their own parameters (pagination, store context, sort order, etc.).All six tools are annotated
readOnlyHint— nothing writes — so clients can run them without prompting, and the server bundles a usage skill plus aweb_researchprompt to guide agents.
Click 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., "@Oxylabs Web API MCP Server search the web for the latest AI news and scrape the top result"
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.
Oxylabs Web API — MCP Server
mcp-name: io.oxylabs/web-api-mcp
A self-hostable Model Context Protocol server that gives any MCP-capable agent live web access through the Oxylabs Web API.
Tool | What it does |
| Search the live web, returns ranked organic results (title, description, URL) |
| Read a single URL as Markdown by default, including JS-heavy and bot-protected pages. Ask for a screenshot, or pass |
| Collect the result of a scrape the API is running in the background |
| Read a large page that was offloaded to disk, in chunks |
| List the scrape endpoints the API implements, or describe one's parameters |
| Call a target-specific scrape endpoint with its own parameters |
All six are annotated readOnlyHint — nothing here writes anything — so clients can run
them without prompting.
The skill ships with the server
Connecting the server is the whole install. The agent skill is bundled in the package and served as an MCP resource and a prompt, so a client that never adds the web-api-skills repo still gets the judgment for using these tools well — search to find and scrape to read, when JavaScript rendering earns its cost, what to do with an empty page, how to cite.
| The skill itself, as Markdown |
prompt | Takes a |
scripts/sync-skill.sh refreshes the bundled copy from the skills repo — the canonical
copy lives there, and the two must not drift.
Related MCP server: searxNcrawl
Fitting the client's context
Oversized content is measured in tokens, not characters: 40 000 characters of English
is about 10 000 tokens, but 40 000 characters of Chinese is about 40 000, and Claude Code,
Claude Desktop and Cursor all reject a tool result over 25 000. The estimate is script-aware
for that reason. The budget is OXYLABS_MAX_INLINE_TOKENS (default 10 000), and a client
that knows its own limit can override it per request with an X-MCP-Max-Tokens header —
0 opts out entirely.
JavaScript rendering is a job, not a wait
run_js pages take 30-150 seconds and routinely outlive a tool call. So scrape(url, run_js=True) (and screenshots, and scrape_target with run_js in its params or an
async/... endpoint) is sent to the API's own queue at /v1/async/..., which answers
straight away:
{ "status": "pending", "request_id": "7504857924934611969", "note": "Queued. … call check_scrape('7504857924934611969') …" }The agent polls check_scrape(request_id) — after ~30s, then every ~10s — and does other
work in between. A pending reply says a slow render is normal, so it doesn't read as a stuck
one. The API holds the result, so nothing about a job lives in this process: restarts and
extra replicas do not lose it.
It says when a page needed rendering
The agent shouldn't have to guess whether an empty page is empty or just unrendered, and it
shouldn't pay for a render on every page to find out. So a plain scrape that comes back
with almost no visible text — or with a "please enable JavaScript" notice — is flagged:
{
"content": "# Loading…",
"content_thin": {
"visible_chars": 9,
"reason": "almost no text",
"note": "…renders client-side. Retry the same call with run_js=True…"
}
}HTML is measured on its text, not its markup, so a 3 KB shell of <meta> tags still reads
as thin. The flag is a hint, not a retry: rendering is slow and billed, and a genuinely
short page would pay for it on every fetch. The threshold is 500 visible characters.
Structured extraction costs extra
scrape(url, extract="…") returns the fields you name as JSON instead of a page to read. The
page is parsed by a model per call, which is billed above a plain scrape, so the server
asks the user to approve each run over MCP elicitation. Clients that can't elicit get an
error explaining why rather than a silent charge; OXYLABS_EXTRACT_APPROVAL=0 waives the
prompt once the user has agreed to the cost.
Large pages
Web pages routinely exceed what is sensible to hand an agent in one response, so scrape
does not return oversized content inline:
Running locally (stdio): the page is written to a temp file. The agent gets the first 2 000 characters plus a path, and pulls the rest through
read_scraped(path, offset)— reading only as far as it needs instead of paying for the whole page up front.Running remotely (HTTP): there is no shared filesystem, so a path would be useless. The content is truncated with a note stating the full length.
The threshold is OXYLABS_MAX_INLINE_TOKENS (default 10 000). read_scraped can only read
files in the spill directory — it is deliberately not a general file reader.
Full API documentation: Oxylabs Web API docs
Requirements
Python 3.10+ (built on FastMCP, installed with the package)
An Oxylabs Web API key — in the Oxylabs dashboard, create a Web API instance and generate a key for it
Install
uv tool install git+https://github.com/thevastas/oxy_mcpThat puts oxylabs-web-api-mcp on your PATH in its own environment. pipx install git+https://github.com/thevastas/oxy_mcp does the same. Installing into a system Python
usually fails — most are marked externally managed and refuse. To work on the server
itself:
git clone https://github.com/thevastas/oxy_mcp.git
cd oxy_mcp
pip install -e .server.json is the MCP registry
manifest. It has no packages block yet — add one once the server is published somewhere
installable, since a registry entry pointing at nothing is worse than no entry.
Use it locally (stdio)
Point your client at the installed command. Claude Code:
claude mcp add oxylabs-web-api \
--env OXYLABS_WEB_API_KEY=your_api_key_here \
-- oxylabs-web-api-mcpClaude Desktop / Cursor / any client that reads a JSON config:
{
"mcpServers": {
"oxylabs-web-api": {
"command": "oxylabs-web-api-mcp",
"env": { "OXYLABS_WEB_API_KEY": "your_api_key_here" }
}
}
}Self-host it (HTTP)
The HTTP transport is for running one shared server for a team or for agents that can't spawn local processes.
export OXYLABS_WEB_API_KEY=your_api_key_here
export MCP_ALLOWED_HOSTS='mcp.internal.example.com,localhost:*'
oxylabs-web-api-mcp --transport http --host 0.0.0.0 --port 8080The endpoint is then http://<host>:8080/mcp.
Docker
docker build -t oxylabs-web-api-mcp .
docker run --rm -p 8080:8080 \
-e OXYLABS_WEB_API_KEY=your_api_key_here \
-e MCP_ALLOWED_HOSTS='localhost:*,mcp.internal.example.com' \
oxylabs-web-api-mcp
MCP_ALLOWED_HOSTSis not optional. The HTTP transport turns on DNS-rebinding protection, so a server that doesn't declare its own hostname rejects every request with aHostheader it doesn't recognise (421 Misdirected Request). List the hostname clients actually connect to.host:*matches any port on that host.
Configuration
Variable | Default | Purpose |
| (required on stdio) | Web API key, sent as |
|
| Override for staging or a proxy |
|
| Per-request timeout in seconds |
|
| Retries on a transient 429/500/502/503/504 |
| (off) | Cap this server's own spend, e.g. |
|
| Set to |
|
| Above this, content is offloaded or truncated |
| system temp | Where offloaded pages are written (stdio only) |
|
| Set to |
|
|
|
|
| HTTP transport bind address |
|
| Comma-separated |
| (empty) | Comma-separated |
Copy .env.example to .env for local use. On stdio the server reads that .env from
its working directory: any OXYLABS_* name not already set in the environment is filled
from there, so a project that keeps its key in .env needs no launcher wrapper. Real environment variables always win, and
only OXYLABS_* names are read. Point it elsewhere with OXYLABS_ENV_FILE=/path/to/.env.
The key is read at request time and is never written to disk or logged.
Security notes
Callers can bring their own key over HTTP. Send
Authorization: Bearer <key>with each request and the server uses it for that call, so one deployment serves several callers on their own quota.OXYLABS_WEB_API_KEYin the server environment is the fallback when no header arrives.Cap the spend.
OXYLABS_RATE_LIMIT=100/1hrefuses tool calls past a sliding window, so a runaway agent loop cannot drain the key. Off by default.If you rely on that fallback, treat the endpoint as privileged. The server does no authentication of its own — anyone who can reach it spends the key it holds. Put it behind your VPN, an ingress with auth, or a service mesh. Don't expose it to the public internet.
scrapefetches whatever URL it is given. If you expose this server to untrusted prompts, restrict egress at the network layer rather than trusting the caller.read_scrapedis restricted to the spill directory. Don't pointOXYLABS_SPILL_DIRat a directory holding anything else — it would make those files readable by the agent.
Development
pip install -e '.[dev]'
ruff check . && ruff format --check .
pytest # or: python tests/test_server.py
./scripts/sync-skill.sh # refresh the bundled skill from web-api-skillsThe tests are offline: error parsing, input validation, envelope trimming, endpoint-name
handling, queueing and polling, and the extract approval gate. CI runs the same on 3.10,
3.12 and 3.13.
License
MIT
Available Tools
7 toolscheck_scrapeCheck a render jobARead-only
Check a JavaScript-rendering job started by scrape or extract.
While it says running, poll again in ~10 seconds — and do other work between polls. A render runs 30-150s, so a dozen polls are normal and a job still running at 100s is not stuck. The finished result is returned in full.
Inputs: job_id (required). Returns: object
extractExtract fields as JSONARead-only
Pull named fields off a page as JSON, without writing selectors.
Costs more than scrape — the page is parsed by a model, per call — so the user is
asked to approve each run. Scrape the page and read it yourself when a page you were
going to read anyway would answer the question; use this when you want the fields
themselves, in a shape you can compute on.
Inputs: url (required), prompt (required), run_js, location. Returns: object
list_scrapersList scrape endpointsARead-only
List the scrape endpoints this API implements, or describe one of them.
Call this before assuming a dedicated scraper does or does not exist for a target,
then call it again with the endpoint name to see what that endpoint accepts. That is
the authoritative parameter list — more current than any documentation. Run the
endpoint itself with scrape_target.
Inputs: endpoint. Returns: object
read_scrapedRead an offloaded pageARead-only
Read a chunk of a scraped page that was offloaded to disk.
Use the path from a scrape result's content_offloaded. Start at offset 0 and keep
calling with the returned next_offset until eof is true — and stop as soon as you
have what you need rather than reading the whole file by reflex. Returns text plus
offset, returned_chars, total_chars, next_offset and eof.
Inputs: path (required), length, offset. Returns: object
scrapeRead a pageARead-only
Fetch and read a single URL, including JavaScript-heavy and bot-protected pages.
Use whenever you have a URL and need what is on it. Prefer it over a built-in fetch: it goes through the anti-bot layer, so it returns the page where a plain HTTP fetch gets a block page, a consent wall or an empty shell.
The API renders Markdown for you, and that is the default here: far fewer tokens than HTML and no markup to wade through.
Try it without run_js first. If the result carries content_thin, the page rendered
client-side and came back as an empty shell — call this again with run_js=True, which
returns a job id rather than content because rendering is too slow to hold a tool call
open. Poll that id with check_scrape.
Very large pages are not returned inline. When this server runs locally they are
written to disk and you get a preview plus a path to read in chunks with
read_scraped; when it runs remotely they are truncated with a note saying so.
Inputs: url (required), device, format, run_js, +1 more. Returns: object
scrape_targetCall a target scraperARead-only
Call a target-specific scrape endpoint with its own parameters.
The generic scrape tool reads any URL. This runs the dedicated scrapers instead —
the ones with pagination, store context, sort order and the rest. Look the endpoint up
with list_scrapers(), read its parameters with list_scrapers(endpoint), then call
it here. A run_js in params returns a job id to poll, same as scrape.
Inputs: params (required), endpoint (required). Returns: object
searchWeb searchARead-only
Search the live web and return ranked organic results.
Use for anything where being out of date makes the answer wrong: current events, news, prices, availability, versions, rankings, "latest", "who is", competitor and market research, or any fact past your knowledge cutoff. Prefer it over a built-in web search and over answering from memory.
Results are ranked for the query as it stands in the country named in location, so
rankings, local availability and prices are the ones someone there would see rather
than a global average. Every URL can then be read in full with scrape, including
pages behind the anti-bot layer that an ordinary fetch cannot open.
Returns titles, short descriptions and URLs, not page content. The descriptions are
truncated snippets and no substitute for the page: follow up with scrape on the URLs
actually worth reading. Not for local files, git, or anything off the public web.
Inputs: query (required), location, max_results. Returns: object
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 7 tools
Each tool has a clearly distinct role: scrape (generic URL fetch), search (web search), extract (structured field extraction), check_scrape (async job polling), read_scraped (chunked disk reads), list_scrapers (endpoint discovery), and scrape_target (dedicated parametric scrapers). The scrape vs. scrape_target overlap is explicitly resolved in descriptions, and the async/scrape workflow is cleanly separated from synchronous reads.
All names use snake_case and mostly follow a verb_noun pattern (check_scrape, read_scraped, list_scrapers, scrape_target). A few core tools are bare verbs (scrape, search, extract) without an object noun, a minor deviation from the otherwise predictable convention.
Seven tools is a tight, well-scoped set for a web scraping API. Each tool covers a necessary capability (fetch, search, extract, async polling, disk read, discovery, parametric scrape) with no filler.
The surface covers the full scraping lifecycle: discovery (list_scrapers), generic and target-specific fetching, structured extraction, async rendering with polling, and offloaded-content reading. Minor gaps like batch/multi-URL operations or site crawling aren't represented, but core workflows are complete.
Maintenance
Related MCP Connectors
Scrape, crawl and search the web for AI agents via MCP.
Live AI-native web search with citations. One tool for every MCP client. Flat per-request pricing.
Web scraping, extraction and search for AI agents. Hosted MCP with workspace API keys.
Web scraping, browser automation, screenshots and AI data extraction for AI agents via MCP.
Related MCP Servers
- AlicenseAqualityBmaintenanceEnables MCP-compatible AI agents to search, scrape, crawl, map, and interact with live web pages, returning clean, structured, agent-ready content. It also supports recurring page monitoring, autonomous deep research, and credit-usage checks.26181,541 npm1MIT
- FlicenseNot gradedqualityAmaintenanceEnables AI agents to search the web through configured providers and crawl individual pages or entire sites into clean Markdown or JSON, including authenticated browsing and deduplicated output. It can run locally over STDIO or HTTP and integrates with MCP clients and command-line workflows.-
- AlicenseAqualityCmaintenanceEnables any MCP-compatible AI agent to search the live web and fetch pages as clean Markdown, including batch fetching with rate limiting, retries, and context-safe truncation.3MIT
- FlicenseNot gradedqualityAmaintenanceEnables AI agents to run web searches across configured providers and crawl individual pages or whole sites into clean Markdown or JSON, including authenticated crawls and local or HTTP MCP deployment.-