remote-mcp-scraper-vercel
README.md
# remote-mcp-scraper-vercel
A production-ready **Remote Model Context Protocol (MCP)** server for **Vercel Serverless on Node.js**, built with **Next.js App Router** and TypeScript.
It exposes scraping-oriented MCP tools over the legacy **HTTP + SSE** transport:
| Endpoint | Purpose |
|---|---|
| `GET /api/sse` | Open an MCP session (`text/event-stream`, 15 s heartbeats) |
| `POST /api/message?sessionId=…` | Send JSON-RPC frames; responses stream back over SSE |
| `GET /api/health` | Liveness/status probe |
## Tools
| Tool | Description |
|---|---|
| `ping` | Liveness probe — server name, version, uptime. |
| `fetch_url` | Fetch an http(s) URL with browser-like headers: rotating real User-Agent profiles, `sec-ch-*` client hints and `Sec-Fetch-*` spoofing, gzip/deflate/br decoding, bounded retries with exponential backoff + `Retry-After` honouring, header/body timeouts, redirect following, and a hard body-size cap. |
| `take_screenshot` | Render a page in headless Chromium and return a PNG/JPEG image plus navigation metadata (final URL, status, bytes, duration). |
| `movie_metadata` | Movie lookup from **legitimate public sources only**: TMDB (`TMDB_API_KEY`), OMDb (`OMDB_API_KEY`), or Wikipedia (no key required). No fabricated results — a miss returns a structured error. |
All tool inputs are validated with strict [zod](https://zod.dev) schemas; outputs include `structuredContent`.
## Stack
- **Next.js 16 (App Router, Node.js runtime)** — route handlers on Vercel Functions
- **@modelcontextprotocol/sdk** — `McpServer` + `SSEServerTransport`, `armSseKeepAlive` heartbeats
- **puppeteer-core + @sparticuz/chromium** — brotli-packed chrome-headless-shell sized for the ~50 MB serverless bundle budget
- **undici** — `request` API (full header control incl. `Sec-*`, unlike `fetch`) + `ProxyAgent`
- **socks** — SOCKS4/5 CONNECT support via a custom undici connector (TLS-in-tunnel for HTTPS origins)
- **zod** — input/output validation
- **vitest + eslint + tsc** — validation (`npm run validate`)
## Project layout
```
app/
page.tsx minimal status page
layout.tsx
api/
sse/route.ts GET — opens MCP session over SSE
message/route.ts POST — JSON-RPC ingress for a session
health/route.ts liveness
lib/
mcp.ts McpServer factory + tool registration
sessions.ts in-memory session registry + TTL sweep
node-shims.ts ServerResponse/IncomingMessage adapters over Web Streams
fetcher.ts stealth fetch client (UA rotation, proxies, retries, caps)
user-agents.ts rotating browser-identity profiles
browser.ts dynamic launcher: CDP → local binary → bundled chromium
movie.ts TMDB / OMDb / Wikipedia metadata providers
schemas.ts zod input schemas
auth.ts optional MCP_AUTH_TOKEN bearer gate
rate-limit.ts per-IP fixed-window limiter
tests/ vitest unit tests
vercel.json function memory (1024 MB) + maxDuration + headers
```
## Local development
```bash
npm install
cp .env.example .env # fill in what you need (all optional for a first run)
npm run dev # http://localhost:3000
```
Validate everything:
```bash
npm run validate # eslint + tsc --noEmit + vitest + next build
```
### Smoke-testing the MCP transport
```bash
# terminal 1 — open the SSE stream, note the sessionId in the `endpoint` event
curl -N http://localhost:3000/api/sse
# terminal 2 — drive the session
SID=<sessionId from the endpoint event>
curl -X POST "http://localhost:3000/api/message?sessionId=$SID" \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"0.0"}}}'
curl -X POST "http://localhost:3000/api/message?sessionId=$SID" \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
curl -X POST "http://localhost:3000/api/message?sessionId=$SID" \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"ping","arguments":{"message":"hi"}}}'
```
Responses arrive as `event: message` frames on the SSE stream.
### MCP client configuration
Any client that supports the SSE transport can connect to `https://<your-deployment>/api/sse`. For `mcp-remote`-style bridges or Claude Desktop:
```json
{
"mcpServers": {
"scraper": {
"url": "https://<your-deployment>.vercel.app/api/sse",
"headers": { "Authorization": "Bearer <MCP_AUTH_TOKEN>" }
}
}
}
```
## Deployment (Vercel)
```bash
npm i -g vercel
vercel # preview
vercel --prod # production
```
`vercel.json` already configures **1024 MB function memory** and a 60 s `maxDuration` for both API routes — required for the bundled Chromium binary. Set environment variables in the Vercel dashboard (or `vercel env add`).
## Environment variables
| Variable | Purpose |
|---|---|
| `MCP_AUTH_TOKEN` | Optional bearer token required on `/api/sse` + `/api/message`. **Set this for any public deployment** — unset means open access. |
| `CDP_ENDPOINT` | Remote Chrome DevTools endpoint (`ws(s)://` → `browserWSEndpoint`, `http(s)://` → `browserURL`). Highest-priority browser path — e.g. Browserless, Steel, self-hosted `chrome --remote-debugging-port=9222`. |
| `PUPPETEER_EXECUTABLE_PATH` | Local Chrome/Chromium binary for development (ignored on Vercel). |
| `SCRAPER_PROXY_URL` | Default upstream proxy for `fetch_url` / `take_screenshot` when the caller passes no `proxy`. |
| `HTTP_PROXY` / `HTTPS_PROXY` | Standard fallback proxy env vars for `fetch_url`. |
| `TMDB_API_KEY` | TMDB v3 key — primary `movie_metadata` provider. |
| `OMDB_API_KEY` | OMDb key — secondary `movie_metadata` provider. |
| `RATE_LIMIT_MAX` | Per-IP tool-call limit per window (0 disables; default 60). |
| `RATE_LIMIT_WINDOW_MS` | Rate-limit window (default 60000). |
| `FETCH_MAX_BYTES` | Response body cap in bytes (default 5 MiB). |
| `FETCH_DEFAULT_TIMEOUT_MS` | Per-request timeout (default 20000). |
| `SESSION_TTL_MS` | Idle-session eviction (default 600000). |
## Proxy configuration
`fetch_url` and `take_screenshot` accept a `proxy` argument and honour the env vars above. Supported schemes:
- `http://` / `https://` — via undici `ProxyAgent` (CONNECT tunnelling, optional auth in the URL).
- `socks5://`, `socks5h://`, `socks4://`, `socks4a://` — via `socks` + a custom undici connector; HTTPS origins get TLS inside the tunnel. Credentials may be embedded (`socks5://user:pass@host:1080`).
- For `take_screenshot`, the proxy is passed to Chromium as `--proxy-server` and embedded credentials are applied with `page.authenticate()`.
Proxy credentials are redacted from all error messages.
## Security & privacy
- **Auth:** optional `MCP_AUTH_TOKEN` bearer (header or `?token=`); no auth is otherwise enforced — treat an unauthenticated deployment as public tooling.
- **Rate limiting:** fixed-window per-IP limiter on tool calls (instance-local, best-effort).
- **Allowlisting:** none — the tools fetch arbitrary http(s) URLs. If you expose this publicly, set `MCP_AUTH_TOKEN` and consider fronting it with your own allowlist; see limitations for SSRF context.
- **No credential leakage:** proxy userinfo is stripped from URLs used in errors, logs, and browser flags.
- **Data handling:** no persistence — request bodies/screenshots live only for the duration of a call; sessions live in instance memory.
## Responsible use
`fetch_url` makes requests look like ordinary browser traffic to avoid *incidental* breakage — it is **not** a bypass for authentication, authorization, paywalls, CAPTCHAs, or site rate-limit policy, and must not be used as one. Honour `robots.txt`, the target site's terms of service, and applicable law. The MCP_AUTH_TOKEN gate and rate limiter are abuse dampers, not access control for the sites you scrape.
## Known limitations
- **Session affinity:** the in-memory session map lives inside one function instance. `POST /api/message` must reach the **same warm instance** as `GET /api/sse`; a cold start or scale-out returns `404 unknown_session`. For multi-instance deployments, switch to the MCP **Streamable HTTP** transport (`StreamableHTTPServerTransport`) with an external session store (e.g. Redis), or keep traffic single-instance.
- **Function lifetime:** SSE connections die at `maxDuration` (60 s default); MCP clients should reconnect and re-`initialize`.
- **Cold starts:** the first `take_screenshot` on a cold instance extracts ~50 MB of brotli-packed Chromium — expect a few seconds; warm calls are sub-second.
- **Binary ceiling:** `@sparticuz/chromium` ships **chrome-headless-shell** — no GPU rendering, no PDF printing; `setGraphicsMode` is disabled.
- **Rate limiter** is per-instance and memory-resident (not distributed).
- **Node ≥ 20.9** required (Vercel `nodejs20.x`+ runtime).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues