# Search files

`search_files`

| Through | Form |
|---|---|
| HTTP | `GET https://api.markdownbrain.ai/v1/search` |
| MCP | tool `search_files` |
| CLI | `mdbrain call search_files` |

Find a string or pattern in the text of a workspace's documents (grep). Returns, per matching file, its `file_id`, its `path`, and the matching lines — each with `line` (1-based), `col_start`/`col_end` (0-based offsets into that line) and a bounded `preview`. **It never returns whole documents**: it tells you where to look, and you decide what to [`read_file`](/docs/operations/read_file). `regex`, `case_sensitive` and `whole_word` all default off, so a plain query is a case-insensitive substring. Narrow with `under` (a folder path) and `name_contains` (case-insensitive substring of the node's own name), which mean exactly what they mean in [`list_files`](/docs/operations/list_files). One workspace per call. `truncated` — on the result and on each file — means **there may be more**, not that you hit the cap: a search can come back with fewer files than `max_files` and still be truncated, so read it as *ask again, narrower* rather than as a count. **Case-insensitive matching is an ASCII promise.** Outside ASCII the database and the matcher fold case by different rules, so a query of `ς` may not find a `σ`; use `case_sensitive` when the exact non-ASCII form is what you mean. It searches saved text, which autosave writes about a second after typing stops.

**Who may call it:** agents and people.

**Writes:** no, it only reads.

## Arguments

| Name | Type | Required | Travels in | Description |
|---|---|---|---|---|
| `workspace_id` | string | yes | query string | Workspace id — one workspace per call |
| `query` | string | yes | query string | The string to find, or the pattern when `regex` is set |
| `regex` | boolean | no | query string | Read `query` as a regular expression (default false) |
| `case_sensitive` | boolean | no | query string | Match the query's own case (default false) |
| `whole_word` | boolean | no | query string | Require a word boundary either side (default false) |
| `under` | string | no | query string | Folder path; search only that subtree. "" or omitted means the whole workspace |
| `name_contains` | string | no | query string | Case-insensitive substring of the node's own name |
| `context_lines` | number | no | query string | Lines either side of a hit in its preview, 0 to 5 (default 0) |
| `max_files` | number | no | query string | Files reported, up to 200 (default 50) |
| `max_matches_per_file` | number | no | query string | Matches reported per file, up to 100 (default 20) |

## Success

`200` with `application/json`.

## Example request

```sh
curl 'https://api.markdownbrain.ai/v1/search?workspace_id=00000000-0000-4000-8000-000000000010&query=invitation&under=specs&max_files=5' \
  -H "Authorization: Bearer $MDBRAIN_API_KEY"
```

```sh
mdbrain call search_files \
  --workspace-id 00000000-0000-4000-8000-000000000010 \
  --query invitation \
  --under specs \
  --max-files 5
```

## Example response

```json
{
  "workspace_id": "00000000-0000-4000-8000-000000000010",
  "query": "invitation",
  "truncated": false,
  "files": [
    {
      "file_id": "00000000-0000-4000-8000-000000000102",
      "path": "specs/12-onboarding.md",
      "truncated": false,
      "matches": [
        {
          "line": 18,
          "col_start": 6,
          "col_end": 16,
          "preview": "Each invitation expires after seven days."
        }
      ]
    }
  ]
}
```

## Refusals

A refusal is a status code with `{ error: { code, message } }`. See [Errors](/docs/errors).
