exray
exray is a remote MCP server that renders web pages and lets you register your own TypeScript scraping and extraction code as reusable named tools.
You do not run or install exray. It's a hosted service at https://mcp.exray.dev/mcp.
The user supplies a bearer token; you call tools over MCP.
Before anything else: is it connected?
If exray tools aren't in your tool list, the MCP server isn't configured. Tell the user to:
- Sign up at
https://app.exray.dev/sign-in— the/welcomepage issues a token (shown exactly once) - Add the server to their client — the config is at https://docs.exray.dev/docs/mcp-clients
- Restart the client — MCP config is read at startup
Do not try to work around a missing connection by scraping with fetch or shell tools.
Say what's missing and stop.
Picking a tool
| Situation | Tool |
|---|---|
| One page, want the content | scrape |
| One page, want specific fields | extract with a flat schema |
| Many pages under a site | crawl, then get_crawl_results |
| The same extraction repeatedly, or logic too complex for a flat schema | write a define_extractor |
| Page needs clicks/scrolling before it renders | write a define_fetcher |
| User wants to look at the results | write a define_handler (result page) |
Reach for scrape / extract first. Only write code when a built-in tool genuinely
can't express what's needed — a registered asset is something the user then has to maintain.
Flat schemas only
extract accepts exactly four types: string, number, boolean, string[]. No nesting.
{ "title": "string", "price": "number", "tags": "string[]" }
Nested or conditional shapes need a define_extractor instead.
Writing code assets
Three kinds, all TypeScript, all validated statically before they're accepted:
- fetcher — entry
fetch(ctx), drives the browser, returns aPageSnapshot - extractor — entry
extract(snapshot, ctx?), turns a snapshot into JSON - handler — entry
handle(request, ctx), serves a public web page
Hard constraints — code violating these is rejected at registration:
- The only import allowed is
import typefrom@exray/exray-api. No runtime imports. If you need DOM querying, work fromsnapshot.htmlwith string handling, or do the DOM work in a fetcher viactx.page.evaluate(). - No
eval/new Function, no top-level side effects - No network access from inside the sandbox
Registration leaves the asset in draft — it cannot run yet. Call publish_definition
before trying to use it. This trips people up constantly; don't report success after
define_* alone.
Result pages — read next_action and act on it
When you publish a handler, the response carries a site object. Publishing
successfully does not mean the page is reachable. Always read site.next_action and tell
the user the specific thing that's still missing:
next_action | What to tell the user |
|---|---|
set_username | "Set a username at app.exray.dev/settings/profile — the address is <username>-<project>.exray.app" |
enable_site | "Your page will be at <site.url>. Enable it: exray site enable, or in the dashboard." |
shorten_slug | "<username>-<slug> is over 63 bytes. Use a shorter project slug." |
contact_support | A server-side configuration problem; report it as such |
null | It's live at site.url — give them the link |
Never finish a handler publish without saying where the page is or what's blocking it. The user has no other way to find out, and "published successfully" reads as "it's done".
What a handler can and can't do:
- Can read this project's job data via
ctx.data(list jobs, fetch a result), return HTML or JSON, route by path - Cannot store anything, reach the network, accept form submissions, or read other projects' data
It's a presentation layer, not a backend.
Storage bindings
Agent code has no persistence by default. If the project has bindings enabled, fetchers get
ctx.kv / ctx.db / ctx.storage; extractors get ctx.kv only; handlers get all three
read-only.
If a call throws binding_not_enabled, the binding isn't turned on — that requires an admin
token (exray bindings set --kv on). Tell the user; you can't enable it yourself.
Errors worth recognising
| Error | Meaning |
|---|---|
403 forbidden | The token lacks a scope. Execution needs tools.execute, registering needs tools.define, publishing needs tools.publish, account actions need admin |
402 | Monthly quota exhausted. Stop and tell the user |
definition_incompatible_api | The asset was compiled against a different @exray/exray-api major. Re-register it |
extractor_invalid / fetcher_invalid | Static validation rejected the code — usually a runtime import or a top-level side effect |
binding_not_enabled | See storage bindings above |
concurrent_exceeded | Too many calls in flight. Wait; don't retry in a tight loop |
On budget_exceeded or 402, stop. Retrying burns the user's quota without progress.
Scopes
A first-scrape workflow needs only tools.read + tools.execute. Registering and publishing
need tools.define / tools.publish. Anything touching the account — issuing tokens,
enabling result pages, toggling bindings — needs admin and is the user's job, not yours.
Full documentation
https://docs.exray.dev — quickstart, client configs, CLI reference, result pages, tool parameters, and the extractor contract.