MCP Guide — the audit tools an AI assistant can call
This guide covers the MCP endpoint (POST /mcp) — a Model Context Protocol server that lets an AI assistant read a client's audits, mark issues fixed, add domains, and manage projects.
Quick Start
Generate a token
In your client portal, go to Domains → API access → Generate token.
Add the server
The portal shows this command pre-filled with your token:
claude mcp add --transport http wsc-crawler https://your-server/mcp \
--header "Authorization: Bearer $TOKEN"Then ask your assistant: "what's still outstanding on the latest audit for example.com?"
Transport
Stateless JSON-RPC 2.0 over a single POST. Deliberately minimal — the endpoint has no server-initiated messages to deliver, so the streaming half of the spec is not implemented.
Authentication
Uses the same per-client token as the REST API. An unauthenticated request gets a 401 Unauthorized with a WWW-Authenticate: Bearer header.
Origin Validation
The Origin header is checked as a DNS-rebinding guard. Native MCP clients (Claude Code/Desktop) send no origin and are allowed by default.
Methods
initializeHandshake. Returns capabilities, server info, and usage instructions.
tools/listAll 29 tools with full JSON Schemas.
tools/callInvoke one tool.
pingReturns an empty result.
resources/listReturns empty — answered politely though not advertised.
prompts/listReturns empty — same.
Version negotiation is permissive: recognized versions include 2024-11-05 through 2026-07-28.
Tools
29 tools mapping 1:1 onto the REST endpoints. All schemas are additionalProperties: false.
Audits
list_domainsGET /domains
add_domainPOST /domains
list_crawlsGET /{domain}/crawls
get_audit_resultsGET /{domain}/{crawlId}/{type}
resolve_issuePOST /{domain}/{crawlId}/resolve
start_auditPOST /{domain}/audit
Monitoring
get_monitoring— statusrequest_monitoring— request schedulecancel_monitoring— cancel slot
Feedback
feedback_overview— all siteslist_feedback— domain itemsset_feedback_status— open/doneadd_feedback_note— internal note
Launch & Migration
list_projectsGET /{domain}/projects
start_projectPOST /{domain}/projects
list_project_tasksGET /{domain}/projects/{type}/tasks
set_task_statePOST /{domain}/projects/{type}/tasks/{taskKey}/state
import_redirectsPOST /{domain}/redirects/import
check_redirectsPOST /{domain}/redirects/check
Time & Account
get_time_reportGET /time/report
log_time_entryPOST /time/entries
get_usageGET /account
Parameters
| Parameter | Type | Notes |
|---|---|---|
| domain | string | Must be on the caller's account. |
| crawlId | string | A crawl UUID, or the literal "latest". |
| type (results) | string | pages, issues, links, images, slow-pages, etc. |
| status (results) | enum | active (default), fixed, ignored, all. |
| mode | enum | basic or advanced (full audit). |
Response Shapes
These are the payloads an agent hits first, transcribed field-for-field from the query columns each operation actually selects.
get_usage
{
"budget": {
"budget": 20000, "used": 4300, "reserved": 0, "remaining": 15700,
"windowStart": "2026-06-15T00:00:00.000Z", "resetsAt": "2026-06-22T00:00:00.000Z"
},
"residentialProxy": { "cap": 5000, "used": 120, "remaining": 4880 },
"monitoring": { "slotsTotal": 10, "slotsUsed": 3, "slotsRemaining": 7 }
}list_crawls
{
"domain": "example.com",
"crawls": [
{
"crawlId": "8f1c2e4a-...", "startTime": "...", "endTime": "...", "status": "Completed",
"mode": "advanced", "pages": 84, "brokenPages": 1, "brokenLinks": 3,
"brokenImages": 0, "brokenCss": 0, "brokenJs": 0, "warnings": 12
}
]
}get_monitoring
{
"domain": "example.com", "monitored": true, "pending": false,
"schedule": "0 6 * * 1", "lastCheckedAt": "2026-06-20T06:00:00.000Z",
"lastCrawlId": "8f1c2e4a-...", "maxPages": 200
}list_feedback (one item)
{
"id": "...", "pageUrl": "https://example.com/pricing", "pagePath": "/pricing",
"note": "typo in the second bullet", "status": "open",
"viewport": "1440x900", "createdAt": "...",
"attachments": [
{ "id": "...", "kind": "screenshot", "mime": "image/webp", "bytes": 48213,
"url": "https://.../api/project/{editToken}/feedback/{attachmentId}" }
],
"targetSelector": ".hero h1", "category": "content", "hidden": false,
"internalNote": null, "noteSentAt": null
}list_project_tasks (one task)
{
"key": "dns-live", "type": "Pre", "category": "DNS", "group": "Go-live",
"task": "Point DNS at the new host", "auto": true, "custom": false,
"edited": false, "state": "open", "source": null, "hidden": false
}list_redirects (one row)
{
"id": "...", "oldUrl": "https://old.example.com/about-us",
"expectedNewUrl": "https://new.example.com/about",
"lastStatus": 301, "resolvedTo": "https://new.example.com/about",
"result": "ok_301", "hidden": false
}Errors
The distinction that matters: a tool that refuses is a successful JSON-RPC call.
Tool Failures (200 OK)
Returns isError: true and a message. The model needs to see why it failed (e.g., rate limit, invalid mode) so it can correct itself.
Protocol Faults
Reserved for things a model cannot fix — malformed JSON (-32700), unauthorized (-32600), or unknown method (-32601).
Two things that will bite you
1. issueId is only valid inside its own crawl
issues.id is re-assigned on everycrawl. An ID from Monday's audit points at a different issue in Friday's — or at nothing. Always pass the crawlId the ID came from.
2. "Fixed" is scoped to one crawl, on purpose
An assistant marking its own homework is a risk. resolve_issuemarks it fixed in the current audit, but a re-crawl will re-detect the issue if the fix didn't really land.The honest loop: fix → resolve → deploy → re-audit to verify.
Implementation Notes
Hand-rolled, no SDK: The MCP handler framing is smaller than an SDK shim. It handles JSON-RPC dispatch directly onto raw node:http responses.
No logic in transport: The MCP handler does framing only; it calls the same internal handlers as the REST API. Auth, ownership, and rate limits exist once.
Rate Limits: Shared with the REST API. Reads (600/15min), Writes (300/15min), and Audits (30/15min).