Back to Directory/Developer Tools

io.github.crawlbrulee/mcp

EU-native web scraping for AI agents. scrape pages, map sites, run background jobs, check usage.

Developer ToolsTypeScriptv1.0.2

๐Ÿฎ crawlbrulee mcp

npm license

EU-native web scraping for AI agents & developers.

plug crawlbrulee into your agent. the official mcp server for crawlbrulee gives mcp-aware agents โ€” Claude Code, Codex, Cursor, Claude Desktop โ€” native tools to scrape pages, map sites, run background jobs, and check usage. one call turns any url into clean markdown, screenshots, metadata and links.

  • everything runs in the EU. the fetch, the render, the cache and your result never leave EU servers. the proxy exit is the one hop you choose: pick an EU exit and nothing leaves at all. gdpr-aligned, with a data processing agreement.
  • output made for models. markdown with the page chrome stripped and the links kept, ready for the prompt. full-page screenshots can come back as tiles sized for an image model.
  • the hard parts, handled. headless Chrome when a page needs it, rotating proxies with country selection, automatic retries, ad and cookie-banner removal, caching, background jobs and signed webhooks.
  • start free. 750 credits, no credit card.

get a free api key โ†’ dashboard.crawlbrulee.com

the server:

  • npx-runnable โ€” zero install.
  • wraps the @crawlbrulee/sdk under the hood; this mcp is just a thin protocol adapter.
  • stdio transport for terminal-based agents.
  • strict, fully-described tool schemas โ€” agents see what every parameter does without reading docs.

this readme covers the mcp server itself โ€” its tools and how to wire it into a host. for how the api behaves โ€” endpoints, parameters, and error semantics โ€” please see our api docs.


install

# Claude Code
claude mcp add crawlbrulee \
  --env CRAWLBRULEE_API_KEY=cwbl_... \
  -- npx -y @crawlbrulee/mcp

# Cursor โ€” add to ~/.cursor/mcp.json:
{
  "mcpServers": {
    "crawlbrulee": {
      "command": "npx",
      "args": ["-y", "@crawlbrulee/mcp"],
      "env": { "CRAWLBRULEE_API_KEY": "cwbl_..." }
    }
  }
}

the same pattern works for Codex, Claude Desktop, and any other host that accepts a stdio mcp launch command โ€” set command: npx, args: ["-y", "@crawlbrulee/mcp"], and forward CRAWLBRULEE_API_KEY via the env block.

configuration

env varrequireddescription
CRAWLBRULEE_API_KEYyesapi key sent as Authorization: Bearer โ€ฆ. get one at https://crawlbrulee.com.

the mcp reads the env var on first tool invocation โ€” not at startup โ€” so a typo in your config surfaces as a clear tool-error message rather than the server failing to come up. see authentication for how the api consumes keys.


tools

scrape

fetch a single url and return the requested content (markdown, cleaned html, raw html, links, images, screenshot, page metadata).

input โ€” only url is required; everything else has sane defaults.

{
  "url": "https://example.com",
  "extract": {
    "markdown": true,
    "links": true,
    "screenshot": { "type": "full_page", "device_mode": "desktop" },
  },
  "require_js": false,
  "proxy": "basic",
  "cleanup": { "ads_and_popups": true, "exclude_selectors": ["nav", "footer"] },
  "cache": { "max_age": 3600 },
  "location": { "locale": "en-US", "country": "US" },
}

output โ€” full scrape result. page metadata (title, OG tags, etc.) is returned under metadata. extracted images are returned as absolute urls โ€” query strings are preserved, and relative srcs are resolved against the page url. screenshots are returned as signed download urls the agent can fetch separately. in rare cases a screenshot can't be captured: when you requested other outputs too, the screenshot field is simply left out while the rest is still returned โ€” but a screenshot-only call that can't deliver errors instead (unsupported_screenshot_output, HTTP 422, when the content type can't be screenshotted) and isn't billed. the result also carries a top-level response_meta.usage block:

{
  "url": "https://example.com",
  "markdown": "...",
  "metadata": { "title": "Example Domain" },
  "response_meta": {
    "usage": {
      "credits": 1,
      "engine": "http", // "http" | "browser" | "screenshot" | "cache"
      "proxy": "basic", // resolved tier actually used: "basic" | "advanced" (never "auto")
      "screenshot_slices": 0, // 1 when the screenshot-split add-on was billed, otherwise 0
    },
  },
}

alongside response_meta.usage, the result surfaces any non-fatal warnings โ€” stable string codes an agent can switch on. an outsized page is truncated rather than refused, and the code names which part was cut:

codewhat it means for the payload
screenshot_truncatedthe page was taller than the scrolling-capture height cap; the screenshot covers the top of the page.
links_truncatedthe page had more than 30,000 links; the links array is cut at the cap and is incomplete.
inline_images_truncatedthe page had more than 10,000 inline images; the images array is cut at the cap and is incomplete.
raw_html_truncatedthe page body exceeded 10,000,000 characters; raw_html is cut at a tag boundary, never mid-tag.
metadata_truncatedthe page head exceeded 2,000,000 characters; metadata can be missing tags that sat past the cut.

and if you requested an extract that doesn't apply to the content type (e.g. markdown of a pdf), the field name comes back in an unsupported_fields list โ€” with the rest of the payload still returned.

every input field, its default, and its constraints are documented under the scrape endpoint โ€” with extraction, screenshots, proxies & location, and caching covering the individual blocks.

scrape_async

submit a scrape job to run asynchronously and get back a job_id immediately, instead of holding the connection open. use this for long-running scrapes (heavy js rendering, full-page screenshots of long pages); for a quick one-shot fetch prefer the synchronous scrape tool. then poll scrape_status until the job is done and fetch the page with scrape_result.

takes the same input as scrape plus an optional per-job completion webhook:

{
  "url": "https://example.com",
  "extract": { "markdown": true },
  "webhook": {
    // Endpoint that receives one signed `scrape.complete` POST when the job
    // finishes. http/https (HTTPS required in production), max 2048 chars.
    "url": "https://hooks.example.com/cwbl",
    // Opaque correlation object echoed back verbatim in the delivery's
    // `data.metadata`. Serializes to at most 2048 bytes.
    "metadata": { "ref": "order-42" },
  },
}

output โ€” { "job_id": "..." }.

when a webhook is attached, we deliver a single signed scrape.complete POST to your endpoint once the job reaches a terminal state, with your metadata echoed under data.metadata and the job's usage under data.response_meta.usage โ€” so you can react to completion (and track cost) without polling. verify the X-Cwbl-Signature header with the sdk's verifyWebhookSignature (configure the signing secret in the dashboard under account โ†’ webhooks).

the job lifecycle is documented under async scrape; the delivery contract and payload shape under webhooks, with the signature scheme in webhook verification.

scrape_status

look up the current lifecycle status of an async job: pending, running, done, or failed (with an error message when failed). once the job is done the response also carries a response_meta.usage block (credits, billed engine, resolved proxy tier, screenshot_slices). a cache hit is represented by engine: "cache". poll until done, then call scrape_result.

{ "job_id": "..." }

scrape_result

fetch the extracted content of a completed async job โ€” the same result shape as the synchronous scrape tool (including metadata and response_meta.usage). errors if the job is still pending/running, so check scrape_status first.

{ "job_id": "..." }

map

build (or fetch a cached) link-map for a website. combines sitemap discovery with homepage link extraction. use this to enumerate a site before scraping selected pages. each link is just { url }.

max_urls (default 5000, max 100000) is a discovery budget, not a trim at the end โ€” discovery stops as soon as that many urls are found, so a smaller value is a faster, cheaper crawl. limit (default 5000, max 10000) only pages the answer.

returned urls are normalized the same way scrape normalizes its returned url, so map-then-scrape stays on one host. results are ordered with the most useful links first.

the response's response_meta carries pagination, truncation, and a usage block (credits, billed engine, resolved proxy tier). map responses do not include screenshot-slice accounting.

{
  "url": "https://example.com",
  "sitemap_only": false,
  "types": { "internal": true, "external": false, "internal_subdomains": true },
  "max_urls": 5000,
  "page": 1,
  "limit": 1000,
}

a map stopped by your own max_urls returns exactly that many links with response_capped: false โ€” the signal that the site has more is truncation.discovery_cap_reason:

{
  "truncation": {
    "storage_capped": false,
    "response_capped": false,
    "total_before_max_urls": 5000,
    "total_detected_before_storage_cap": 5000,
    "discovery_capped": true, // discovery stopped before reading every sitemap file
    "sitemaps_skipped": 3, // files skipped or only partly read
    "discovery_cap_reason": "max_urls", // retry with a higher max_urls
  },
}

discovery_cap_reason is one of max_urls, time, file_budget, depth, file_size, unread_files, or null when nothing stopped discovery. only max_urls is a limit you can raise from the request. unread_files means a sitemap file the site publishes could not be read at all this time โ€” often temporary, so asking again later can return more. time, file_budget, depth and file_size mean the site itself is big, slow or deep, and a retry will not help.

see the map endpoint for discovery rules and pagination semantics.

usage

returns the current billing-cycle snapshot: total / used / available credits, used quota percent, max concurrency, and cycle reset timestamp. takes no arguments. what a call costs, and how credits are counted, is documented under credits & pricing.

whoami

returns the organization name, token name, and truncated token preview for the configured api key. useful for confirming which account is in use before credit-consuming operations.


errors

every tool returns an mcp error result (isError: true) when the api call fails. the error text follows a stable format:

[<errorName>] <message> (HTTP <status>)

agents can branch on the errorName code. the set comes from the sdk's ApiErrorName union plus two synthetic codes added by this mcp (missing_api_key, internal_error):

codemeaning
missing_api_keyCRAWLBRULEE_API_KEY is not set in the mcp host's env.
invalid_credentialsserver rejected the api key (revoked, wrong env, etc.).
service_unavailabletemporary backend failure (HTTP 503). your key is fine โ€” retry with backoff.
too_many_requestsrate limit hit โ€” back off and retry.
usage_allocation_errorplan credit / concurrency cap exceeded. show usage to user.
validation_errorinput failed server validation.
invalid_urltarget url was rejected before fetching.
blocked_urltarget url is on the blocklist.
antibot_blockedorigin's anti-bot defenses blocked the fetch.
too_many_redirectsorigin redirected the fetch in a loop (HTTP 422). the target's doing โ€” don't retry blindly.
page_too_largethe page's html was too large to process (HTTP 422). terminal โ€” never retry it.
scrape_errororigin returned an error during scraping.
unsupported_screenshot_outputscreenshot-only request on a content type that can't be screenshotted (HTTP 422). not billed.
not_foundasync job ID unknown (e.g. bad job_id to scrape_status / scrape_result).
request_timeoutnetwork / read timeout. safe to retry.
client_closed_requestcaller cancelled before completion.
internal_server_errorunhandled server-side failure.
crawlbrulee_errorsdk error without a typed name.
internal_errorbug in this mcp โ€” please open an issue.

the api docs carry the canonical error reference โ€” every error name, what causes it, and how to recover.


development

pnpm install
pnpm typecheck   # tsc --noEmit
pnpm lint        # eslint
pnpm test        # vitest run
pnpm build       # tsup โ†’ dist/index.js with shebang
pnpm verify      # all of the above

run the built mcp locally:

CRAWLBRULEE_API_KEY=cwbl_... node ./dist/index.js

it will block waiting for an mcp client on stdio. combine with the MCP Inspector for interactive debugging.

docs

this readme covers the mcp server itself โ€” installing it, wiring it into a host, and the tools it exposes. for how the api behaves โ€” endpoints, parameters, and error semantics โ€” the api docs are canonical. the mcp guide covers host setup in more depth.

part of the crawlbrulee toolkit

one api, many ways to call it:

  • js/ts sdk โ€” @crawlbrulee/sdk (the sdk this mcp wraps)
  • python sdk โ€” crawlbrulee on pypi
  • cli โ€” npx crawlbrulee
  • mcp server โ€” @crawlbrulee/mcp (this one)
  • agent skills โ€” for skills-aware coding agents

docs: crawlbrulee.com/docs ยท dashboard: dashboard.crawlbrulee.com

license

Apache-2.0

Installation

Source-derived launch command. Check the maintainerโ€™s required arguments and credentials before running:

bash
npx -y @crawlbrulee/mcp

Set up in your AI client

Merge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.

json
{
  "mcpServers": {
    "io-github-crawlbrulee-mcp": {
      "command": "npx",
      "args": [
        "-y",
        "@crawlbrulee/mcp"
      ]
    }
  }
}

Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the clientโ€™s tool list, then try a read-only example from its documentation.

Claude Desktop setup reference

Package

@crawlbrulee/mcpnpm

Compatible MCP Clients

io.github.crawlbrulee/mcp works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.

  • Claude Desktop~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.
  • Cursor~/.cursor/mcp.jsonRestart Cursor for changes to take effect.
  • VS Code.vscode/mcp.jsonReload VS Code window for changes to take effect.
  • Windsurf~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect.
  • Claude Code.mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.

Learn More