# Errors and Contract

Read the HTTP error responses, the read-only boundary, and the fields the search response guarantees.

## Error responses

| Condition | HTTP status | Example body |
|---|---|---|
| Missing `db` | 400 | `{"error":"db is required"}` |
| Unknown or unloaded `db` | 400 | `{"error":"\"name\" not exist"}` |
| Missing or empty `q` | 400 | `{"error":"invalid argument: q is required"}` |
| Unknown `target` | 400 | `{"error":"invalid argument: unknown target \"bogus\""}` |
| Search, embedding, or registry failure | 500 | `{"error":"..."}` |
| `/mcp` while `remote` is disabled | 404 | Gin default not-found body |

The `db` checks run in router middleware before the search handler. Every other argument error is wrapped in `search.ErrInvalidArgument`, which the handler maps to 400; anything else becomes 500. If either concurrent search branch fails, the request returns an error rather than a partial result.

## Read-only boundary

The router exposes no mutation routes. `/mcp` accepts the HTTP methods the protocol transport requires, but the two tools behind it are read-only. To add content, place files in a registered inbox and let the watcher-controlled indexing pipeline write to SQLite; see [Indexing Pipeline](/indexing-pipeline).

## API response contract

Search results are grouped by source in first-seen order and expose only:

```json
{
  "source": "/path/to/file.md",
  "matches": [
    {"chunk": 1, "content": "..."}
  ]
}
```

| Field | Meaning |
|---|---|
| `source` | Absolute path of the indexed file inside the database inbox (`~/.config/kuradb/<db>/inbox/...`), not the `~/Kura_<db>` symlink path |
| `matches[].chunk` | Chunk index within that file |
| `matches[].content` | Chunk text |

Internal fields such as row ID, semantic score, keyword hit count, chunk total, and cache state remain implementation details and are never serialized. Consumers should rely on result order, not on a score.
