Read, crawl and act on websites, signed in as the user, and turn any site into an API.
One address for any MCP client. No sign-in needed on this server.

4 read10 act25 other
Grouped by tool name. At most: moves money, runs code or commands, deletes data, or sends messages for you.
Odel charges nothing per call. The publisher may charge for its own service.
Quick start · Tools · Configuration · Security · Troubleshooting · Contributing
writ-mcp connects a stdio MCP client — Claude Code, Claude Desktop, Cursor, Windsurf, Codex — to Writ, so the browser workflows you already recorded become tools your assistant can call: run them, read the data they collected, search past results, schedule them, expose them as REST endpoints, and kick off site crawls.
It is a transparent stdio↔HTTP proxy and nothing else. Every tool, schema and rule lives server-side, so what you get always matches what your instance can do — and a new tool never requires upgrading this package. Zero dependencies, one file, Node core only.
Clients that speak Streamable HTTP natively don't need this at all — point them straight at the
/mcpendpoint with anAuthorization: Bearer <key>header.
1. Get an API key. In the Writ app: Settings → Developers → API keys. Keys
look like wt_….
2. Add the server.
Against a self-hosted coordinator:
claude mcp add writ-selfhost -e WRIT_API_KEY=<YOUR_API_KEY> -- npx -y writ-mcp --url https://writ.example.com
Against a coordinator on this machine:
claude mcp add writ-selfhost -e WRIT_API_KEY=<YOUR_API_KEY> -- npx -y writ-mcp --url http://localhost:8000
Against a published per-workflow endpoint (an "Expose as MCP" slug URL is used verbatim — no path rewriting):
claude mcp add my-tools -e WRIT_API_KEY=<YOUR_API_KEY> -- npx -y writ-mcp --url https://mcp.example.com/mcp/my-tools
3. Check it.
claude mcp list
Writ Cloud is the default target when you pass no
--url(https://api.usewrit.app). Pass--urlto point the connector at your own coordinator instead. See Status.
Pass the key through the environment, not the command line.
--api-keyworks, but it puts your key in the process's argument list where any local process can read it viaps, and your shell records it in history. The connector prints a note when you use it.
Your running coordinator also hands out these one-liners, pre-filled, on its
Connect page and at GET /api/mcp/connect-info:
Add to claude_desktop_config.json (Claude Desktop) or ~/.cursor/mcp.json
(Cursor). The env form is recommended — it keeps the key out of the argument
list:
{
"mcpServers": {
"writ-selfhost": {
"command": "npx",
"args": ["-y", "writ-mcp"],
"env": {
"WRIT_COORDINATOR_URL": "https://writ.example.com",
"WRIT_API_KEY": "<YOUR_API_KEY>"
}
}
}
}
A self-host install bundles this connector at connectors/writ-mcp. There is no
build step, so you can run it straight from disk:
node /path/to/writ/connectors/writ-mcp/index.js --url https://writ.example.com
It coexists with the other Writ servers. Each surface registers under its own slug on purpose — the desktop app is
writ, Writ Cloud iswrit-cloud, a self-hosted coordinator iswrit-selfhost— and each identifies itself to the assistant with a distinct title. Keep any combination connected at once.
Served by the server you connect to, not by this package, so a new server tool reaches you without a connector update. Writ Cloud lists the 39 tools below; a self-hosted coordinator lists the ones marked in its column.
| Tool | What it does | Writ Cloud | Self-hosted |
|---|---|---|---|
writ_list_workflows | Your saved workflows (a run_<name> tool is added only for workflows you pin) | yes | yes |
writ_run_workflow | Run one and wait for the extracted data | yes | yes |
writ_pin_workflow_tool | Pin or unpin a workflow as its own run_<name> tool — off by default, capped; everything still runs via writ_run_workflow | yes | yes |
writ_update_workflow | Read a workflow's outline or edit it in place (settings, steps, functions, its agent skill) | yes | yes |
writ_diagnose_http_workflow | Show what every request of a run was answered with, and whether it ran without a browser | yes | no |
writ_create_http_extraction | Author a multi-request browserless extraction from captured requests | yes | no |
writ_workflow_data | Read a workflow's accumulated data table | yes | yes |
writ_search_data | Search across everything already collected | yes | yes |
writ_export_data | Export a workflow's data as CSV/JSON | yes | yes |
writ_workflow_runs | Run history and status | yes | yes |
writ_set_schedule | Schedule a workflow (interval / daily / weekly) | yes | yes |
writ_expose_workflow_api | Publish a workflow as a callable REST endpoint | yes | yes |
writ_scrape | Read a page, or several, as clean markdown (Cloud also reads a listing's top N) | yes | yes |
writ_crawl_site / writ_crawl_status | Start and poll a distributed site crawl | yes | yes |
writ_crawl_files | The original documents a crawl captured (PDFs, office files, images) | yes | no |
writ_saved_crawls / writ_run_saved_crawl / writ_saved_crawl_data | Saved crawls (see below) | yes | yes |
writ_update_saved_crawl | Inspect or change a saved crawl's settings | yes | no |
writ_personas | See your saved sign-in identities and get them ready (see below) | yes | yes |
writ_payment | Pay with a card the user approves; Writ places the order once they confirm | yes | no |
writ_devices | Your linked Writ desktops: list them, or point this connection at one | yes | no |
writ_create_automation | Event or clock → run a workflow, notify, or wake an agent | yes | yes |
writ_list_webhooks | Your inbound webhook URLs and the automations they fire | yes | yes |
writ_create_monitor / writ_wire_monitor | Watch a page and react to changes | yes | yes |
writ_browser_use | Open a real browser your assistant drives turn by turn | yes | yes |
writ_record_website | Record a repeatable task as a workflow | yes | yes |
writ_website_to_api | Turn a website into a callable API | yes | yes |
writ_discovery_status | Follow an API build to its result | yes | no |
writ_browser_act / _context / _network / _save / _cancel | Act in the browser, read the page and its network calls, save or close the session | yes | yes |
writ_browser_compose | Define named functions, inputs and explicit steps in a session | yes | no |
writ_browser_ask_user | Hand a CAPTCHA, a code or a decision to the user in the live browser | yes | no |
writ_browser_sessions | Open browser sessions you can resume | yes | no |
writ_record_start / _act / _context / _network / _save / _cancel | Older names of the browser loop, kept for compatibility | no | yes |
The build tools (writ_browser_use, writ_record_website, writ_website_to_api, then
writ_browser_act / _context / _network / _save / _cancel) are on both targets;
neither has a writ_build tool, so use writ_record_website. Your
assistant opens a real browser, drives it turn by turn, and saves the session as
a reusable workflow that afterwards replays with no model in the loop. Writ Cloud
runs it on Writ's fleet; a self-hosted coordinator runs it on your own fleet
agent. Either way your assistant is the
brain — no second model key is involved.
max_age)Running a workflow drives a real browser, so asking the same question twice in one
session costs two full runs and two waits. Every workflow tool takes an optional
max_age (seconds) meaning a recent answer is good enough:
{ "name": "run_price_check", "arguments": { "sku": "B0C123", "max_age": 300 } }
0 — always run fresh (the default; nothing goes stale on its own).N — reuse a result younger than N seconds, otherwise run.A reused answer carries _cache: {hit: true, age_seconds: N} so the assistant can
tell how current it is.
max_age works identically against Writ Cloud and a self-hosted coordinator, and on
a pinned workflow's own run_<name> tool as well as writ_run_workflow.
writ_run_saved_crawl)A whole-site crawl is slow and metered, so re-crawling to answer the same question is
the most expensive mistake an assistant can make. A saved crawl is a stored crawl
configuration with a stable name, and the same max_age contract applies to it:
{ "name": "writ_run_saved_crawl", "arguments": { "crawl": "docs", "max_age": 86400 } }
writ_crawl_status (a crawl outlives a single tool call).Three tools cover the surface:
| Tool | What it does |
|---|---|
writ_saved_crawls | List saved crawls. Check here before crawling a site again. |
writ_run_saved_crawl | Run one, reusing recent data when max_age allows. |
writ_saved_crawl_data | Read what one already collected, at any age. Never crawls. |
To create one, pass save_as to writ_crawl_site — that saves the settings and
runs them, so the crawl becomes callable by REST as well. Re-using the same save_as
updates that saved crawl instead of piling up duplicates. Saving needs an
admin-scoped credential (it creates reusable, callable configuration); running one
needs only run.
writ_personas)A persona is a saved sign-in identity — username, credentials sealed
server-side, optional 2FA, and a warm signed-in session. writ_personas lists
yours (with per-persona readiness), inspects one (get), refreshes its session
by running its login workflow (sign_in), or has the AI record the sign-in flow
once so the persona can sign itself in from then on (record_login). The
assistant then passes a persona_id to writ_run_workflow (and each pinned
run_<name> tool), writ_crawl_site, or writ_scrape — plus writ_browser_use
on Writ Cloud — and the task runs signed in, with 2FA codes minted server-side.
By design the connector can only use personas, never manage them: creating, editing or deleting one involves credentials, and no credential or one-time code ever crosses the MCP connection in either direction. Personas are created in the Writ dashboard.
It comes back as status: "running" with retryable: true. The run was not
cancelled — calling the tool again starts a second run. Wait, then retry with a
max_age wide enough to pick up the first run's result once it lands.
Flags take precedence over environment variables.
| Flag | Env | Default | Purpose |
|---|---|---|---|
--url | WRIT_COORDINATOR_URL / WRIT_URL | https://api.usewrit.app | Target base URL. A URL whose path is already /mcp or /mcp/<slug> is used verbatim. |
--api-key | WRIT_API_KEY | — (required) | Key for Authorization: Bearer. Prefer the env var. |
--insecure | WRIT_INSECURE_TLS=1 | off | Accept a self-signed local-CA cert. Private networks only. |
--timeout | WRIT_MCP_TIMEOUT_MS | 600000 | Per-request timeout (ms); covers long writ_run_workflow waits. |
--help / --version | — | — | Print usage or version and exit. |
HTTPS with a local CA (self-host): trust the CA (recommended) and use the
https://address, or setNODE_EXTRA_CA_CERTS=/path/to/ca.pem. Use--insecureonly for localhost testing — it disables certificate verification entirely, which exposes your key to a man-in-the-middle.
This process exists to carry a credential, so everything that could expose one is
made loud rather than convenient. Full detail in SECURITY.md.
--url and nowhere else. No telemetry, no analytics, no
update check. Zero dependencies means there is no transitive code in the
process that could phone home — and CI fails the build if that ever changes.Authorization header to
whatever origin a Location header names would hand your key to a host you
didn't choose. A 3xx becomes an error telling you to point --url at the final URL.https://user:pass@host/ would otherwise
be written down in plaintext.http:// to a non-loopback host, or a key passed on the
command line.tools/call is sent exactly once, because a retry could re-execute a side
effect the connector cannot see.Scope your keys. Give a key only workflows:read / workflows:execute unless
a tool you actually use needs more.
Releases are published from CI with npm provenance, so the tarball is cryptographically linked to the commit and workflow that built it:
npm audit signatures
| Symptom | Cause and fix |
|---|---|
Unauthorized: … rejected the API key | The key is wrong, disabled, or lacks scope. Recreate it under Settings → Developers with workflows:read / workflows:execute. |
Cannot reach … | Wrong --url, or the target is down. For a self-signed cert see the local-CA note above. |
… redirected (HTTP 301) | Your reverse proxy redirects (usually http → https). Point --url at the final URL. |
The API key contains characters that cannot be sent in an HTTP header | A newline or control character got into the key — usually a copy-paste artifact. Re-copy it. |
| No tools listed | The connector could not reach the coordinator, or the key was rejected — see the rows above. With a working connection the static writ_* tools are always listed, even before you save a workflow. |
A workflow has no run_<name> tool | Derived tools are opt-in and off by default. Pin the workflow with writ_pin_workflow_tool (or the app's Expose as MCP tool switch) and it appears on the next tools/list, up to 20 per account. Every workflow runs through writ_run_workflow either way. |
| Client won't connect, no error | Read the connector's stderr — your client logs it. Claude Code: ~/Library/Caches/claude-cli-nodejs/<project>/mcp-logs-<name>/. |
| Self-hosted coordinator | Supported and verified end to end. |
Published /mcp/<slug> endpoints | Supported. |
Writ Cloud (https://api.usewrit.app, the no---url default) | Supported. Used automatically when no --url is passed. |
npm test
No dev dependencies — the suite uses Node's built-in node:test and drives the
real index.js as a subprocess against a mock MCP server, exercising the same
stdio path an MCP client uses. It runs in about six seconds. npm publish runs it
automatically via prepublishOnly.
See CONTRIBUTING.md — note the two hard rules: zero
dependencies, permanently, and no tool logic here.
| usewrit/writ | The self-host coordinator — web UI, API, your data. Start here. |
| usewrit/writ-agent | The Rust fleet worker that does the actual browsing. |
| writ-mcp (this repo) | The MCP connector. |
MIT — see LICENSE.
This package is deliberately permissive because it runs inside your MCP client, not inside the coordinator, so it has to be embeddable anywhere. The coordinator it talks to is AGPL-3.0-only; the two licenses are not interchangeable.