MCP server
OpenIntents is a remote Model Context Protocol
server. Any MCP client can use it: sign in from the client or add the server
with your API key, 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.
https://api.openintents.io/mcpThere 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 fromOPENINTENTS_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
/mcponly, never on the REST API, and you can revoke it under Connectors in the dashboard. Without a token the server answers401with aWWW-Authenticateheader 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 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:
claude mcp add --transport http openintents https://api.openintents.io/mcpWith a key:
claude mcp add --transport http openintents https://api.openintents.io/mcp \
--header "Authorization: Bearer $OPENINTENTS_API_KEY"Codex
codex mcp add openintents --url https://api.openintents.io/mcp \
--bearer-token-env-var OPENINTENTS_API_KEYClaude Desktop, Cursor and other clients
Add it to the client's MCP config (for Cursor, ~/.cursor/mcp.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, 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 (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.
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) tocreate_intentoranswer_input, or callwait_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.
initializeanswers with anMcp-Session-Id; send it back on later requests (an idle session ends after 24 hours, andDELETE /mcpwith the ID ends it at once). Atools/callwhoseAcceptheader namestext/event-streamis answered as a stream, so that a call which holds on an intent can report progress first. Ask for it by putting aprogressTokenin the request's_meta: the call then sendsnotifications/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-25that supports tasks can callcreate_intentas a task and carry on:tasks/getreads the intent (working,input_required,completed,failed,cancelled),tasks/resultholds for up to 50 seconds and returns the finished intent, andtasks/cancelcancels it. The task ID is the intent ID.
Or skip MCP for the waiting and use a webhook.
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 userinput_request.promptand send their answer withanswer_input. Never answer for them. A question about an identity document (kind: sensitive) is never answered through a tool: give the userinput_request.url, where they type it themselves. Apaymentquestion (pay_at_shop) shows the shop's UPI QR code atinput_request.image_url: show it to the user, and answerdoneonce they say they paid.requires_login: the shop wants the user signed in. Give themsign_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.reasonsays which). Give thempayment.payment_urlrather than trying to pay; the intent carries on by itself once they're done. See Wallets & payments.
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.
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.