# CrawlCheck MCP tool lockfile, v1

A lockfile pins the tools an operator approved on an MCP server. Before every connect, the client hashes the server's current `tools/list` the same way and refuses the server when anything differs. The approved tool is then the tool that runs, or nothing runs.

## Make one

`GET https://crawlcheck.io/api/v1/mcp/lock?url=<endpoint>` (CrawlCheck runs the handshake and `tools/list`, calls no tool) or `POST {tools: [...]}` with a list you already hold. The answer is the lockfile, signed (kind `crawlcheck-mcp-lock`). Save it beside the server's configuration.

## Format

```json
{
  "kind": "crawlcheck-mcp-lock", "v": 1,
  "server": {"url": "https://mcp.example.com/mcp", "protocol_version": "2025-06-18", "info": {"name": "example", "version": "1.4.0"}},
  "approved_at": "2026-10-07T10:45:00.000Z",
  "tools_sha256": "…",
  "count": 3,
  "tools": [{"name": "create_issue", "sha256": "…"}, {"name": "get_issue", "sha256": "…"}, {"name": "list_issues", "sha256": "…"}],
  "descriptions": {"create_issue": "…"},
  "scan_at_approval": {"severity": "none", "kinds": {}, "findings": []},
  "sha256": "…", "signature": "…"
}
```

## Hashing (no server, no key)

1. For each tool take `{name, description, inputSchema, annotations}` (absent fields as `null`).
2. Canonical JSON: object keys sorted, no whitespace, strings as JSON escapes them. SHA-256 of the UTF-8 bytes = the tool's hash.
3. Sort the tool hashes, join with `\n`, SHA-256 = `tools_sha256`.

A client that does this before each connect needs nothing from CrawlCheck. `POST /api/v1/mcp/lock/check {lock, tools}` (or `{lock, url}`) returns the same comparison signed (kind `crawlcheck-mcp-lock-check`): `verdict` unchanged | changed | unreachable, `decision` connect | block, the tools added, removed and modified, and for a changed description the text that moved.

## Rules

- Any difference blocks. A new tool, a removed tool, a changed description, a changed schema, a changed annotation all count: a changed description is the usual shape of a rug pull.
- Unreachable blocks. A lockfile proves what was approved, not that the server is still that server.
- Re-approval is a person's act. The client replaces the lockfile only when someone has read the new list; the check answer's `modified_detail` shows what to read.
- Hashes cover what the model is handed. Server-side behaviour is outside the lockfile; a tool that keeps its description and changes what it does is a different problem (see the task canaries).
