# MCP server

OpenIntents is a remote [Model Context Protocol](https://modelcontextprotocol.io)
server. Any MCP client can use it: sign in from the client or add the server
with your [API key](/dashboard/api-keys), and your agent gets tools to create
and track intents. It speaks streamable HTTP, and the protocol revisions
`2025-11-25`, `2025-06-18`, `2025-03-26` and `2024-11-05`.

```text
https://api.openintents.io/mcp
```

There are two ways to sign in:

- **An API key**, sent as a header, `Authorization: Bearer oi_…`. Start with a
  test key (`oi_test_…`): test intents run end to end but never charge. The
  commands below read it from `OPENINTENTS_API_KEY`.
- **OAuth**, for clients that support it (Claude Code, Claude Desktop and
  others): add the server with just its URL. The client finds the sign-in on
  its own, opens your browser, and you pick the account and test or live mode
  on a consent page. The token it gets works on `/mcp` only, never on the
  [REST API](/docs/api), and you can revoke it under Connectors in the
  [dashboard](/dashboard). Without a token the server answers `401` with a
  `WWW-Authenticate` header that points the client to the sign-in.

Either way the tools work on one account, in the mode of the key or of the
approval. `/mcp` and `/v1` share one [rate limit](/docs/api#rate-limits) per
key or connector.

## Add it to your client

### Claude Code

With OAuth, no key is needed. Add the server, then run `/mcp` in Claude Code
and choose Authenticate:

```bash
claude mcp add --transport http openintents https://api.openintents.io/mcp
```

With a key:

```bash
claude mcp add --transport http openintents https://api.openintents.io/mcp \
  --header "Authorization: Bearer $OPENINTENTS_API_KEY"
```

### Codex

```bash
codex mcp add openintents --url https://api.openintents.io/mcp \
  --bearer-token-env-var OPENINTENTS_API_KEY
```

### Claude Desktop, Cursor and other clients

Add it to the client's MCP config (for Cursor, `~/.cursor/mcp.json`):

```json
{
  "mcpServers": {
    "openintents": {
      "url": "https://api.openintents.io/mcp",
      "headers": { "Authorization": "Bearer oi_test_..." }
    }
  }
}
```

## Tools

| Tool | What it does |
| --- | --- |
| `create_intent` | Start an intent. Arguments mirror [`POST /v1/intents`](/docs/api#create-an-intent), plus an optional `idempotency_key` and `wait_seconds`. |
| `wait_for_intent` | Wait until the intent is final or pauses, up to 50 seconds, then return it. Call it again while it is still queued or running. |
| `get_intent` | Fetch an intent's status and result. |
| `list_intents` | Recent intents, filterable by status and customer. |
| `get_intent_events` | An intent's public timeline: what the agent did, step by step. |
| `answer_input` | Answer what an intent asked (`requires_input`): a value, a form's `values`, a decline, or done. Takes `wait_seconds` too. |
| `cancel_intent` | Cancel an intent that hasn't finished. |
| `create_secret` | Store a payer's [secret](/docs/vault) (an address, contact details, a shop password). It is sealed and never returned. |
| `list_secrets` | A payer's secrets as labels, masks and public fields, never their values. |
| `delete_secret` | Delete a secret and its values for good. |

Every tool works on the key's own account only. Each has an input schema and an
output schema made from the REST API's own request and response types, so a
field added to `POST /v1/intents` shows up in `create_intent` too. Tools that
only read carry `readOnlyHint`, and `cancel_intent` and `delete_secret` carry
`destructiveHint`. A failed call returns an error result (`isError: true`)
whose text is the same error body the REST API sends.

Customers, sessions and profiles are not MCP tools yet: manage them through
the [REST API](/docs/api).

## Waiting for an intent

An intent takes a few seconds to minutes. There are three ways to follow one,
and a call that stops waiting loses nothing: the intent runs on.

- **Wait in the call.** Pass `wait_seconds` (0 to 50, default 0) to
  `create_intent` or `answer_input`, or call `wait_for_intent`. It returns the
  intent as it stands when it is final, pauses for somebody, or the time is up;
  call it again to keep waiting. Fifty seconds is the longest because the
  connection would otherwise sit silent for longer than the load balancer
  allows.
- **Progress and sessions.** `initialize` answers with an `Mcp-Session-Id`;
  send it back on later requests (an idle session ends after 24 hours, and
  `DELETE /mcp` with the ID ends it at once). A `tools/call` whose `Accept`
  header names `text/event-stream` is answered as a stream, so that a call
  which holds on an intent can report progress first. Ask for it by putting a
  `progressToken` in the request's `_meta`: the call then sends
  `notifications/progress` (the status and the newest event) before the result.
  Without a token the stream carries only keep-alive comments until the
  result.
- **Tasks.** A client on `2025-11-25` that supports tasks can call
  `create_intent` as a task and carry on: `tasks/get` reads the intent
  (`working`, `input_required`, `completed`, `failed`, `cancelled`),
  `tasks/result` holds for up to 50 seconds and returns the finished intent,
  and `tasks/cancel` cancels it. The task ID is the intent ID.

Or skip MCP for the waiting and use a [webhook](/docs/webhooks).

## When an intent pauses

An intent sometimes needs the user before it can go on. The tool result says
which, and what to give them:

- **`requires_input`**: the shop asked for something only the user knows: the
  phone number or email they sign in with, a one-time code, an approval, or a
  detail of the order (a size, a seat, a form of several fields). Ask the user
  `input_request.prompt` and send their answer with `answer_input`. Never
  answer for them. A question about an identity document (`kind: sensitive`)
  is never answered through a tool: give the user `input_request.url`, where
  they type it themselves. A `payment` question (`pay_at_shop`) shows the
  shop's UPI QR code at `input_request.image_url`: show it to the user, and
  answer `done` once they say they paid.
- **`requires_login`**: the shop wants the user signed in. Give them
  `sign_in.url`, a link for them alone, and wait.
- **`requires_payment`**: the user has a step to take in the wallet their
  purchases are paid from: connect it, add money, raise a limit or confirm who
  they are (`payment.reason` says which). Give them `payment.payment_url`
  rather than trying to pay; the intent carries on by itself once they're
  done. See [Wallets & payments](/docs/wallets#when-a-purchase-waits).

The server tells connecting clients this too, so a model handles it without
being asked.

If your client supports **elicitation** and holds the call open (a
`wait_seconds`, or `wait_for_intent`, on a stream), a plain question about the
order is asked in the client itself, as a form, and your user's answer goes
straight to the intent. Questions about the user's sign-in are never asked
this way, since they are about the user's account and not your agent's: the
user answers them at `input_request.url`.

<Callout type="tip">
  Agents do best when told the budget. _"Book an Uber to SFO at 7am, max $60"_
  becomes `create_intent` with `max_amount: 6000`. Give each purchase its own
  `idempotency_key`, and send it again on a retry: a retried call never buys
  twice.
</Callout>
