> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trycactus.com/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP Server

> Connect Claude, Cursor, or any MCP client directly to the Cactus API.

The Cactus Partner API ships a built-in [MCP](https://modelcontextprotocol.io)
server at `https://api.trycactus.com/mcp`. Point an MCP-capable agent at it
with your API key and the agent can upload documents, run extractions, start
underwritings, and check usage — the same operations as the REST API, with
the same authentication, billing, and rate limits.

Use a **sandbox key** (`ck_sandbox_...`) while building: every tool works
identically, results are instant fixtures, and nothing is billed. See
[Sandbox](/sandbox).

<Info>
  **Connection details** — URL `https://api.trycactus.com/mcp`, transport
  streamable HTTP, auth header `Authorization: Bearer ck_...`. There is no
  package to install and nothing to run locally; it's a remote server.
</Info>

## Install

<Tabs>
  <Tab title="Claude Code">
    ```bash theme={null}
    claude mcp add cactus https://api.trycactus.com/mcp \
      --transport http \
      --header "Authorization: Bearer ck_sandbox_..."
    ```

    Add `--scope project` to write the server into the repo's `.mcp.json` and
    share it with your team — keep the key out of a committed file by passing
    `--header "Authorization: Bearer ${CACTUS_API_KEY}"`, which Claude Code
    expands from your environment.

    Verify with `claude mcp list`, or run `/mcp` inside Claude Code: `cactus`
    should show as connected with 13 tools.
  </Tab>

  <Tab title="Claude Desktop / claude.ai">
    Settings → Connectors → **Add custom connector**:

    * **Name**: Cactus
    * **URL**: `https://api.trycactus.com/mcp`
    * Under advanced settings, add a header `Authorization` with the value
      `Bearer ck_sandbox_...`

    Save, then confirm Cactus appears in the connectors list for a new chat with
    its tools enabled.
  </Tab>

  <Tab title="Cursor">
    Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (per project):

    ```json theme={null}
    {
      "mcpServers": {
        "cactus": {
          "url": "https://api.trycactus.com/mcp",
          "headers": {
            "Authorization": "Bearer ck_sandbox_..."
          }
        }
      }
    }
    ```

    Cursor picks the file up on save; Settings → MCP lists the server and its
    tools.
  </Tab>

  <Tab title="Other clients">
    Any client that speaks streamable HTTP works. The generic configuration is:

    ```json theme={null}
    {
      "mcpServers": {
        "cactus": {
          "type": "http",
          "url": "https://api.trycactus.com/mcp",
          "headers": {
            "Authorization": "Bearer ck_sandbox_..."
          }
        }
      }
    }
    ```

    For a client that only speaks stdio, bridge with
    [`mcp-remote`](https://www.npmjs.com/package/mcp-remote):

    ```json theme={null}
    {
      "mcpServers": {
        "cactus": {
          "command": "npx",
          "args": [
            "-y", "mcp-remote", "https://api.trycactus.com/mcp",
            "--header", "Authorization: Bearer ck_sandbox_..."
          ]
        }
      }
    }
    ```
  </Tab>
</Tabs>

<Warning>
  Your API key authorizes real, billable work. Only configure it in agents you
  control, and prefer sandbox keys anywhere an agent runs unattended.
</Warning>

## Your first extraction

With a sandbox key connected, no file upload is needed — the
[example documents](/sandbox#example-documents) are extractable by filename
alone. Ask the agent:

> Using the Cactus tools, register `example-rent-roll-1.xlsx` as a rent roll,
> extract it, and summarize the unit mix.

It will call `upload_document` → `create_extraction` → `get_extraction` →
`get_extraction_result` on its own. The server describes those flows to the
client on connect, so you don't have to spell out the sequence.

Moving to a live key changes two things: the agent must `PUT` the real file
bytes to the presigned URL from `upload_document` before starting a job, and
extractions take minutes instead of returning instantly.

## Tools

Every tool maps onto a documented REST endpoint and returns the same JSON.

| Tool                        | Endpoint                          |
| --------------------------- | --------------------------------- |
| `upload_document`           | `POST /v1/documents`              |
| `list_documents`            | `GET /v1/documents`               |
| `get_document`              | `GET /v1/documents/{id}`          |
| `get_document_download_url` | `GET /v1/documents/{id}/download` |
| `delete_document`           | `DELETE /v1/documents/{id}`       |
| `create_extraction`         | `POST /v1/extractions`            |
| `list_extractions`          | `GET /v1/extractions`             |
| `get_extraction`            | `GET /v1/extractions/{id}`        |
| `get_extraction_result`     | `GET /v1/extractions/{id}/result` |
| `create_underwriting`       | `POST /v1/underwritings`          |
| `list_underwritings`        | `GET /v1/underwritings`           |
| `get_underwriting`          | `GET /v1/underwritings/{id}`      |
| `get_usage`                 | `GET /v1/usage`                   |

Two ergonomic differences from the raw API:

* `create_extraction` takes the three bundle document ids as flat parameters
  (`offering_memorandum_document_id`, `rent_roll_document_id`,
  `t12_document_id`) instead of a nested `bundle` object. Its `asset_class`
  parameter is required, exactly as on the endpoint - the tool description
  tells the agent to ask you rather than guess.
* `create_underwriting` takes the property address as flat parameters
  (`address_line1`, `city`, `state`, `postal_code`, ...).

Each write tool also accepts an optional `idempotency_key`, with the same
semantics as the `Idempotency-Key` header on the REST endpoints.

`get_extraction` and `get_underwriting` also take `wait` — see below.

With a live key, `upload_document` returns a presigned `upload.url`; the
agent (or you) must HTTP `PUT` the raw file bytes to it before starting a
job — the MCP server never handles file contents itself. Sandbox documents
skip the upload entirely.

## Waiting for results

With a live key, extraction and underwriting tools return immediately with
`status: processing` — the job runs in the background. Getting the waiting
part right is the main thing that separates a smooth agent run from a noisy
one, so the server does most of it for you.

**Use `wait` instead of sleeping.** `get_extraction` and `get_underwriting`
accept `wait` (0-45 seconds). The API holds the request open and answers the
moment the job reaches a terminal state, so a typical extraction takes a
couple of tool calls and no timer:

```
get_extraction(extraction_id="ext_...", wait=45)
```

This matters more for agents than for ordinary HTTP clients. Agent runtimes
differ in how — and whether — they can sleep between tool calls, and one that
sleeps wrong turns into a tight polling loop that reads the same `processing`
back dozens of times. `wait` removes that failure mode entirely.

**A `wait` that elapses is not a timeout.** You get a normal response with
the job still in flight. Call again; nothing is lost and nothing is
double-billed.

**In-flight responses tell you when to come back.** While a job is running
the body carries `poll_after_seconds` (the recommended interval, mirrored in
the `Retry-After` header) and `elapsed_seconds` (how long it has been
running). Prefer those to a hard-coded interval — status changes no faster
than `poll_after_seconds`, so polling harder just burns rate limit.

**Expected durations and hard ceilings:**

| Job                          | Typical                                | Ceiling    |
| ---------------------------- | -------------------------------------- | ---------- |
| Extraction — T-12, OM, other | 2-5 minutes                            | 60 minutes |
| Extraction — rent roll       | 5-10 minutes, up to 30 for a large one | 60 minutes |
| Underwriting                 | 20-30 minutes                          | 4 hours    |

Past the ceiling the job fails, and any billed lines are refunded in full. A
job still reporting `processing` has not silently died.

**Rent rolls run in two phases**, which is why they are the slow path. They
extract, then consolidate into the typed `rent_roll.v1` output, and both
phases report `status: processing`. `documents[].phase` distinguishes them
(`extracting` → `consolidating`), so a job moving between phases is visibly
progressing rather than stuck. An agent that gives up after a few minutes
will abandon rent rolls that were going to succeed.

Sandbox keys complete everything instantly, so none of this applies there —
which is also why an agent flow that looks instant in sandbox needs testing
against a live key before you trust its waiting behaviour.

## Billing and limits

Tool calls are ordinary API calls. Live extractions debit your usage pool at
acceptance and refund automatically on failure; underwriting runs are not
metered. The same rate limits apply either way. Ask the agent to call
`get_usage` for the current balance.

Underwritings are the long path and run for \~20-30 minutes. Keep unattended
agents on a sandbox key.

## Troubleshooting

<AccordionGroup>
  <Accordion title="Tools fail with 'No API key'">
    The client connected but isn't sending the header. Check that the value
    includes the `Bearer ` prefix and that it's configured as a *header* —
    not a URL query parameter or an OAuth setting.
  </Accordion>

  <Accordion title="401 or 403 on every tool">
    The key itself was rejected. `401` means it's unknown or revoked; `403`
    means it lacks the scope for that operation. See
    [Authentication](/authentication).
  </Accordion>

  <Accordion title="The client can't connect at all">
    Use the exact URL `https://api.trycactus.com/mcp` — no trailing slash —
    with streamable HTTP transport (not SSE, not stdio). A plain browser
    `GET` of that URL returning `405` is expected; it's a POST endpoint.
  </Accordion>

  <Accordion title="A tool call times out">
    Requests are capped at 60 seconds. Extractions and underwritings are
    asynchronous by design: `create_*` returns immediately and the agent
    polls `get_*`. Retry a timed-out poll — no work is lost. `wait` is
    capped at 45 seconds so a long poll always answers inside the cap.
  </Accordion>

  <Accordion title="The agent polls in a tight loop, or waits far too long">
    It's guessing an interval. Have it pass `wait` to `get_extraction` /
    `get_underwriting` and follow `poll_after_seconds` from the response
    rather than sleeping on its own schedule — see
    [Waiting for results](#waiting-for-results).
  </Accordion>

  <Accordion title="A job sits at 'processing' and looks stuck">
    Check `elapsed_seconds` against the ceilings above, and
    `documents[].phase` for rent rolls — `consolidating` means the second
    phase is running normally. Jobs cannot exceed their ceiling; past it
    they fail and refund in full, so `processing` always means still
    working.
  </Accordion>
</AccordionGroup>

## Docs search

This documentation site also exposes its own MCP server at
`https://docs.trycactus.com/mcp` (search and read the docs — no API access).
Connect both to give an agent the reference material and the ability to act
on it.
