# MCP server

Give AI agents Spaw's verification tools over the Model Context Protocol: one endpoint, your existing API key, the same billing.

Spaw speaks the Model Context Protocol (https://modelcontextprotocol.io), so an AI agent in Claude Code, Cursor, Visual Studio Code, Codex or any other MCP client that can send an Authorization header can verify email addresses, inspect domains, validate phone numbers, look up IP addresses, check postal addresses, read business identifiers against public registers and compare several signals against each other as tools, one at a time or a whole list in one call. Same API keys, same billing, same answers; agent usage shows up on your Usage page as `mcp · key name`.

## Connect a client

The server lives at `https://spaw.co/mcp` over streamable HTTP, authenticated with a secret API key as a bearer token. Create a secret key on the API keys page and export it as `SPAW_KEY` in the environment your agent starts from: the commands and the Cursor link below read the key from that variable, and VS Code asks for it instead. Getting started explains how keys work (https://spaw.co/docs#api-keys).

Use a secret key, never a publishable one — the agent runs server-side on your behalf. Requests share the API's rate limit of 5 per second per account.

### Claude Code

```bash
claude mcp add --transport http spaw https://spaw.co/mcp \
  --header "Authorization: Bearer $SPAW_KEY"
```

The shell expands `$SPAW_KEY` when the command runs (in bash or zsh; in PowerShell write `$env:SPAW_KEY`, or use the single-quoted form below), so export it first; Claude Code then stores the key in its own configuration. Single-quote the header instead, `'Authorization: Bearer ${SPAW_KEY}'`, and Claude Code stores only the reference, reading the variable from its environment each time it connects. Keep `--header` after the name and the URL: it takes several values, so a name placed after it is read as another header. Claude Code adds the server to the current project only; add `--scope user` to use it in every project.

### Cursor

[Add to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=spaw&config=eyJ1cmwiOiJodHRwczovL3NwYXcuY28vbWNwIiwiaGVhZGVycyI6eyJBdXRob3JpemF0aW9uIjoiQmVhcmVyICR7ZW52OlNQQVdfS0VZfSJ9fQ%3D%3D) opens Cursor with the server filled in. Cursor fills in `${env:SPAW_KEY}` from its own environment when it connects (some earlier releases sent it literally, so update Cursor if the server answers 401); export `SPAW_KEY` in your shell profile and restart Cursor after changing it. If nothing happens, Cursor may not be installed or the browser blocked the link; add the server by hand, in `~/.cursor/mcp.json` for every project or `.cursor/mcp.json` for one:

```json
{
  "mcpServers": {
    "spaw": {
      "url": "https://spaw.co/mcp",
      "headers": { "Authorization": "Bearer ${env:SPAW_KEY}" }
    }
  }
}
```

### VS Code

[Install in VS Code](vscode:mcp/install?%7B%22name%22%3A%22spaw%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fspaw.co%2Fmcp%22%2C%22headers%22%3A%7B%22Authorization%22%3A%22Bearer%20%24%7Binput%3Aspaw-key%7D%22%7D%2C%22inputs%22%3A%5B%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22spaw-key%22%2C%22description%22%3A%22Spaw%20secret%20API%20key%22%2C%22password%22%3Atrue%7D%5D%7D) opens VS Code with the server filled in. VS Code asks for the key the first time the server starts and keeps it in its secret storage, so nothing needs exporting. If nothing opens, VS Code may not be installed (VS Code Insiders takes `vscode-insiders:` links) or the browser blocked the link; add the server by hand, in your user `mcp.json` (run MCP: Open User Configuration from the Command Palette) or in `.vscode/mcp.json` for one workspace. VS Code's file lists servers under `servers`, not `mcpServers`, and the header takes the key from the input declared under `inputs`:

```json
{
  "servers": {
    "spaw": {
      "type": "http",
      "url": "https://spaw.co/mcp",
      "headers": { "Authorization": "Bearer ${input:spaw-key}" }
    }
  },
  "inputs": [
    {
      "type": "promptString",
      "id": "spaw-key",
      "description": "Spaw secret API key",
      "password": true
    }
  ]
}
```

### Codex

OpenAI's Codex adds the server with one command:

```bash
codex mcp add spaw --url https://spaw.co/mcp \
  --bearer-token-env-var SPAW_KEY
```

It writes this to `~/.codex/config.toml`, which the Codex CLI, the Codex IDE extension and the ChatGPT desktop app share:

```toml
[mcp_servers.spaw]
url = "https://spaw.co/mcp"
bearer_token_env_var = "SPAW_KEY"
```

Codex stores the variable's name, not the key, and reads it each time it connects. If `SPAW_KEY` is unset or empty, Codex refuses to start the server and names the variable. A desktop app started from the Dock or the Start menu sees `SPAW_KEY` only if it is set in the environment the app launches with.

### Other clients

Any client that connects to remote MCP servers over streamable HTTP and can send a custom header works: send `Authorization: Bearer` followed by your secret key to `https://spaw.co/mcp`. Where that goes differs by client (VS Code's file uses `servers` where Cursor's uses `mcpServers`), so follow your client's own documentation.

ChatGPT on the web adds MCP servers as apps that sign in with OAuth or use no authentication; this server offers no OAuth, so it cannot be added there, while the ChatGPT desktop app reads the Codex configuration above. claude.ai's custom connectors expect OAuth too, except in Anthropic's request-headers beta for a limited set of organizations: choose No sign-in and add an Authorization request header whose value is Bearer followed by a secret key, which everyone in the organization then shares.

A missing or wrong key answers 401. Some clients, Cursor and VS Code among them, then try an OAuth sign-in and report an OAuth or registration error rather than a bad key, so check the key, or the variable that holds it, first.

## Key controls

The scopes, daily credit cap and address allowlist a secret key was created with (https://spaw.co/docs#api-keys) apply to tool calls exactly as they apply to REST requests. Give an agent its own key, scoped to what it is there to do.

- **Scopes.** A tool needs the scope of the product family it belongs to, the same one its REST endpoint needs: the three email tools need `email`, the IP tools `ip`, the phone tools `phone`, the address tools `address`, verify-entity needs `entity`, check-consistency needs `consistency`, and account-balance needs `account`. A tool the key is not scoped for answers a tool error naming `KEY_SCOPE_DENIED` — something the agent can read and act on, rather than a failed connection it can only report.
- **Daily credit cap.** What each tool call bills is added to the key's counter, and once the cap is spent, further calls answer `KEY_SPEND_CAP_REACHED` and run nothing. A batch tool that reaches the cap part-way down its list stops there, keeps the rows already paid for and reports `meta.stopped_reason: "key_spend_cap"`.
- **Address allowlist.** A key with an allowlist is refused from any other address before anything runs — `403 KEY_IP_NOT_ALLOWED`, not even the tool listing. A leaked agent key is as useless here as it is on the REST API.

## Tools

Twelve tools, mirroring the REST endpoints. Four of them take a list, so an agent checks two hundred numbers in one call rather than two hundred calls. An agent that runs out of credits gets a clear tool error, not a half answer.

| Tool | Arguments | Behavior |
| --- | --- | --- |
| verify-email | email | One full verification — verdict, risk score, and every documented field. 1 credit for a deliverable or risky verdict; unverified answers and 7-day repeats free, undeliverable free within fair use. |
| verify-email-batch | emails (≤50) | Compact per-address rows under the same billing. Suppressed addresses answer free; running dry mid-batch returns the paid partial results. |
| domain-intelligence | domain | MX, provider, SPF/DMARC, and disposable/free-provider lists for a whole domain. 1 credit when it accepts mail; dead or invalid domains free. |
| lookup-ip | ip, privacy? | Locate and risk-score an IPv4 or IPv6 address: location, network and operator type, tri-state datacenter/Tor/VPN/relay flags with the provider named, is_anonymous, and the published risk score. Pass "me" for the calling address. One credit covers five fresh answered lookups; reserved and unallocated ranges and 7-day repeats free. |
| lookup-ip-batch | ips (≤1,000), privacy? | The same answer for a whole list, in input order, each row carrying its position and the address as submitted. Every item must be an address — "me" is the single tool only. Five answered addresses cost one credit, and reserved and unallocated ranges, unknown addresses and 7-day repeats are free, so a thousand addresses cost at most two hundred credits; running dry mid-list returns the paid rows with stopped_reason of insufficient_credits or key_spend_cap. |
| verify-phone | phone, country?, hlr?, client_ip?, call_window? | Validate and enrich a phone number: E.164 and display forms, country facts, line type and carrier with the source that settled them, the regulator's block status where a table is installed, the fictional, virtual and disposable flags, and the published risk score. 1 credit for a valid number; invalid numbers, unassigned blocks and 7-day repeats free. hlr asks for the live carrier-network check. The live carrier-network check (`hlr: true`) is not enabled on the service: `hlr_checked` answers false and nothing extra is charged. |
| verify-phone-batch | phones (≤1,000), country?, hlr?, call_window? | The same answer for a whole list, in input order, each row carrying its position and the number as submitted. An item is a number or an object { phone, country }, and its own country wins over the list-level one. Suppressed numbers answer free; running dry mid-list returns the paid rows with stopped_reason. |
| verify-address | address \| fields, country, deliverability? | Read one postal address against its own country's format: valid with a typed reason, the address split into its parts and re-rendered the way that country writes it, the postcode checked against the national directory, the delivery point named (street, po_box, private_mailbox, general_delivery, rural_route, military, parcel_locker, bulk_mail), the national register's answer with its published coverage, how many companies are registered there, and the published risk score. Send one written address or the separated fields, always with country. 1 credit for a valid answer; addresses that cannot stand and 7-day repeats free. Signals whose dataset is not installed answer null, never false, and nothing says who lives or works there. |
| verify-address-batch | addresses (≤100), country?, deliverability? | The same answer for a whole list, in input order, each row carrying its position and the parts that were given. An item is a written address or an object of the separated fields, and its own country wins over the list-level one; an item left with no country is answered missing_country, free. Suppressed addresses answer free; running dry mid-list returns the paid rows with stopped_reason. |
| verify-entity | identifier, type, country? | Check a business identifier against locally synced registers: a Legal Entity Identifier, or a UK company number with its country. Answers the legal name, whether the entity is still live, the country and jurisdiction of formation, the legal form and the incorporation date; a LEI adds its check digit, registration status and renewal date, because a registration can lapse while the entity behind it is alive. Where both registers carry the same entity they are compared field by field. 1 credit when a register answers; a malformed identifier, one no register carries, and a register that is not installed are free. No registered address is stored or returned. |
| check-consistency | email?, phone?, country?, ip?, address?, address_country? | Run any subset of an email address, a phone number, an IP and a postal address in one call and name where they agree and where they do not. Each signal answers exactly what its own tool answers and bills the same; the analysis on top is free: country agreement plus named flags, each quoting both sides. A comparison is emitted only when both of its signals answered, and the country comparisons are withheld for a VPN, relay or datacenter IP, whose country belongs to the network operator. No score, no verdict — a difference between two fields is a fact about the fields, not a judgement about a person. |
| account-balance | — | The current credit balance and monthly grant. Free — useful before a batch. |

Field meanings and billing details are the same as the REST API — see the API reference (https://spaw.co/docs/api) and the email verification guide (https://spaw.co/docs/email-verification).

## Related

- API reference: https://spaw.co/docs/api
- Verify an email address over REST: https://spaw.co/docs/api/verify-email
- Validate a phone number over REST: https://spaw.co/docs/api/validate-phone
- Look up an IP address over REST: https://spaw.co/docs/api/lookup-ip
- Validate a postal address over REST: https://spaw.co/docs/api/validate-address
- Address verification guide: https://spaw.co/docs/address-verification
- Email verification guide: https://spaw.co/docs/email-verification
- Getting started: https://spaw.co/docs

Reference: https://spaw.co/docs/mcp
