Connect your AI
Every instance serves two MCP servers. The public one at /mcp is for customers’ agents and needs no login. The owner MCP at /mcp/owner is for you and the AI you trust: it lists what needs a person, shows an item’s full story, moves items through their states, replies to customers and edits settings. It is stateless per request (MCP spec 2026-07-28), and its tools are generated from the same schemas as the REST API.
Two ways in: an API key, or OAuth 2.1 with a login.
Today: an owner API key
Section titled “Today: an owner API key”An owner key is a bearer token that starts with sdi_own_. On your server, from the directory the server runs in or with INBOX_DB set, so the key goes into its database (--env-file-if-exists reads a .env there when you have one):
node --env-file-if-exists=.env apps/inbox/dist/server.mjs create-owner-key laptopThe key is printed once and stored hashed. It is the same key the owner app takes at /login. Send it as Authorization: Bearer sdi_own_… to /mcp/owner or to any /v1/owner/* route; for Claude Code, for example:
claude mcp add --transport http --header "Authorization: Bearer sdi_own_…" inbox https://<your-instance>/mcp/ownerAny MCP client that can send a header works the same way. Agent keys (sdi_agent_…) exist for customers’ agents that want a stable identity on the public doors.
Without a command line, on a Cloudflare install for example, sign in to the owner app by email and create a key under Settings → Keys. Keys made there are integration keys: named, limited to the scopes you pick, and revocable in one click. Give each system its own; see Keys and scopes.
OAuth 2.1
Section titled “OAuth 2.1”The instance is its own authorization server, so a consumer product connects with a login rather than a pasted key (ADR-004). A client discovers it from the protected resource:
GET /.well-known/oauth-protected-resource/mcp/ownerGET /.well-known/oauth-authorization-serverWhat it supports: authorization code with PKCE (S256, required); public clients only; Client ID Metadata Documents first, which means the client_id is the https URL of a JSON document describing the client, fetched by the instance and cached; dynamic client registration (POST /oauth/register) as the fallback for clients that still use it; opaque hashed tokens; access tokens that last an hour and refresh tokens that last thirty days and rotate on every use, with reuse detection; one consent screen listing the scopes in plain words.
Anyone can register an app, because assistants register themselves when they connect, so the consent screen doesn’t take an app’s word for its name. It shows where the app’s access goes (the host its sign-in comes back to), marks a self-registered app’s name as not verified, and warns when an app calls itself Claude, ChatGPT, Gemini, Grok, Copilot or Muse but sends access somewhere else. Only that page can answer it: an Allow posted from another site is refused. Each time you allow an app the inbox emails you, and Settings → Keys lists every connected app with a Disconnect button that revokes its tokens at once. Self-registered apps nobody signed in with are forgotten after a week.
Scopes: inbox:read, inbox:write, events:read, catalogue:write, availability:write, settings:read, settings:write, setup:run, integrations:write, keys:write, offline_access. A client that asks for none is offered every one of them but keys:write (keys are yours in person, whatever an app holds), and the consent screen lists each. A call outside what you allowed is refused, 403 not_allowed with details.reason: "outside_scopes" and the scope it needed, and is listed under Settings → Keys; to give an app more, disconnect it there and connect it again. That is Refuse calls outside a key’s scopes, on out of the box; off, such a call goes through and is only listed. An app that asked for none before calls outside the scopes were refused was given only inbox:read inbox:write settings:read offline_access, and keeps that: Settings → Keys says so beside it, and connecting it again lets it set the business up.
In the history and in every event, the AI appears as itself: actor owner_ai with the app’s name, not as you.
Endpoints: /oauth/authorize, /oauth/token, /oauth/register, /oauth/revoke. The authorization step needs an owner session in the browser, which comes from a sign-in email (POST /auth/magic-link; then the page its link opens, whose button posts to /auth/verify, or its six-digit code posted to /auth/code; the first address to sign in on a fresh instance becomes its owner). The owner app offers both ways in: the sign-in email, which creates that session, and an owner API key for people who only ever use the API.
What has run. On 1 November 2026, Gemini CLI 0.66 and the MCP Inspector 2.13 worked an inbox run on our own machine through the owner MCP, with a key and with a token from its OAuth sign-in, which a script of ours went through rather than either client. Claude Code 2.1.72 connected and found the sign-in; no Claude model ran yet. The steps below for Claude, ChatGPT, Gemini, Grok and Copilot Studio are their makers’ own, and none of them has run against an inbox yet.
Claude
Section titled “Claude”In Claude, go to Settings, then Connectors, and add a custom connector with your owner MCP URL, https://<your-instance>/mcp/owner. Claude fetches the metadata, sends you to your instance to sign in and approve the scopes, and connects. Claude uses a Client ID Metadata Document, so nothing needs to be registered by hand.
ChatGPT
Section titled “ChatGPT”In ChatGPT, enable developer mode in the connector settings, then create a connector with the same URL and choose OAuth. Sign in on your instance when asked.
Gemini
Section titled “Gemini”On a computer, open gemini.google.com, then Settings, then Connected Apps. Under Custom apps, choose Add a custom app, paste https://<your-instance>/mcp/owner and approve it in the browser; it then works in the Gemini mobile app too. On Gemini Enterprise, a team adds it under Manage team, Connected apps, Add MCP Server.
Grok and Grok Bot
Section titled “Grok and Grok Bot”In Grok, go to grok.com/connectors, choose New Connector, then Custom, and paste the owner MCP URL (custom connectors are on xAI’s paid plans). In Grok Bot, say “Add a custom MCP server called My shop at https://<your-instance>/mcp/owner” and complete the sign-in on the card it shows; its plugins then work in every bot you make. Both take OAuth, or an API key as a header: Authorization with the value Bearer sdi_own_….
Meta Muse
Section titled “Meta Muse”Muse can build a custom connector from what a service publishes: your instance’s /openapi.json and its public MCP server at /mcp are what it reads to book, quote and order for its person. Meta’s reviewed directory is a separate submission to Meta, in the US and Canada.
Copilot Studio
Section titled “Copilot Studio”In a Copilot Studio agent, add a tool, choose an existing Model Context Protocol server, and give it the owner MCP URL with OAuth (dynamic client registration) or an API key header. Microsoft 365 Copilot’s own federated connectors are read-only for now.
Other clients
Section titled “Other clients”Cursor, VS Code and any other client that speaks Streamable HTTP with OAuth 2.1 take the same sign-in; Cursor and VS Code have not run against an inbox yet. Clients that cannot do OAuth use an API key. Both MCP servers answer every protocol revision from 2025-03-26 to 2026-07-28. Step by step for each assistant: Assistants.
What the owner tools do
Section titled “What the owner tools do”There are 44 owner tools:
| Tool | Does |
|---|---|
list_items |
Items newest first, filtered by type, state or needs_human, or searched with q. |
get_item |
One item with its typed fields, event history, conversation and the valid next transitions. |
transition_item |
Fire one of the events the item lists (confirm, propose, decline, quote, …); pass expected_version to avoid racing a colleague. A cancellation the customer asked for by email or phone is record_cancel, with their words as the note: it counts as theirs, not yours. A time you proposed is booked when the customer accepts it; your AI cannot record a yes they gave by phone, only a person can (confirm in the owner app). |
open_return |
A customer asked you, by email or phone, to send back goods of a fulfilled order: write it down as a return (why, and what they said). A withdrawal they wrote is transition_item record_withdrawal, naming their message (entryId). Your AI may approve a return inside your policy, the goods coming back; refusing one, disputing what came back and refunding are yours. |
make_offer |
Propose as the item’s state allows: another time for a booking, changes to an order (quantities, a delivery date), a quote, or a change to a confirmed booking or an accepted order, which stays as agreed if the customer says no. Your AI changes times, quantities and delivery, never a price or a quote, and after a few rounds of back and forth leaves the rest to you. It asks a customer for a change only if you let it (Settings → Changes to what was agreed); a customer’s own change it accepts with transition_item accept_change, or declines with decline_change. |
list_offers |
Every offer on an item: the customer’s request, what you proposed, each answer, what was agreed, and until when each held. |
reply |
Send a reply to the customer, or an internal note with internal: true. A reply your AI sends reaches the customer with one line in your words: “This reply was sent automatically. Reply to reach a person.” get_item shows whether each email went out. |
export_customer |
Everything the inbox holds about one customer, as one document, for a customer who asks what you have about them. Erasing a customer is yours alone, in the app: your AI cannot. |
stop_customer_networks |
For a customer who asks not to be known to booking networks: from then on nothing about them goes to any network. |
get_profile, update_profile |
The business’s name, domain, time zone, currency and languages. The currency is yours to change. |
list_services, upsert_service, archive_service |
Bookable services. A price is per booking unless it is per person. A service your AI adds with a price is saved unpublished for you to check; it cannot change a price or publish a priced service. |
list_products, upsert_product, archive_product |
Products. A product your AI adds is saved unpublished for you to check; it cannot change a price or publish a product, nor how a price is meant, a “was” or unit price, an age, or a product’s shipping and return terms. |
upsert_product_group |
A product with variants (sizes, colours, roasts): your AI may make the group and give each variant its options. |
get_availability, set_opening_hours, clear_service_hours, set_closures |
Opening hours and closed days. |
list_rules, list_rule_presets, apply_rule_preset, upsert_rule, delete_rule, test_rule |
What happens on its own. |
list_webhooks, create_webhook, update_webhook, rotate_webhook_secret, delete_webhook, send_test_event, list_webhook_deliveries, replay_webhook_delivery, replay_missing_webhook_deliveries, list_events |
Where events go, and the event stream. Adding, changing, rotating or removing an endpoint is yours, in Settings → Integrations: your AI can list, test, replay and pause one. See Webhooks. |
list_feeds, add_feed, import_feed_now, remove_feed |
Product feeds. A feed sets prices, so only you connect or remove one: your AI reads them and imports one again. See Feeds. |
list_integrations, sync_integration |
Connected calendars: what can be connected and what is, each one’s busy times, bookings written and last error, and a sync now. A connection holds your password or sign-in and decides which times customers are offered, so only you connect, change or disconnect one, in Settings → Integrations: your AI leaves you a note. See Calendars. |
list_api_keys, create_api_key, revoke_api_key |
Integration keys. Only you create and revoke them, in Settings → Keys: your AI lists them and tells you which one a system needs. |
get_settings |
The settings document and its version, without its secrets. |
update_settings |
Change settings: send only the sections that change, with expected_version; the rest keeps its values, and null removes a key. Networks are keyed by origin, so adding or switching off one leaves the others alone. Where alerts and emails go, switching webhooks or test mode on, the inbound email secret, who is trusted, and switching a network on are yours to do in the owner app. |
get_networks |
The networks this inbox reports to and how each is doing: last ping, last error, the rules each applies, receipts published, waiting and held until it takes newer rules (changes agreed to a booking or order, and refunds, need rules version 6). |
The server’s instructions to the model are short: never invent facts about availability or prices; read them first; give each system its own key; send an idempotency_key with every write, so a retry never does it twice; record a customer’s own cancellation with record_cancel, never cancel_by_business; a customer who asks for their data gets export_customer, one who asks not to be known to booking networks stop_customer_networks, and erasing one is yours; money is yours. Every refusal names the fields to fix.
Time yes, money no
Section titled “Time yes, money no”Your AI confirms bookings, proposes other times and answers customers, and does with money only what you let it, in code rather than in its instructions (Settings → Prices, and what your AI may agree to). Out of the box: any free time within seven days of the one asked for, at your catalogue’s price for that customer, and nothing else. What it offers outside your limits — a discount, a quote, a price the catalogue does not give, a time further away or outside your opening hours, a later delivery, a dearer price than you last offered, past the last round — is not sent: you find it as a draft on the item, with what it is outside, to send as it is, change or drop, and you are emailed at most once an hour for it. A reply it writes that names an amount of money you have not offered (in figures or in words; a catalogue price only beside what it prices), or something off a price, is kept as a note for you the same way. Only you send or drop a draft. It never learns your lowest prices, your limits or your rewards as numbers, so a customer cannot talk it into reciting one; it only learns, as a code, that something is outside them. It never records a payment, a failed payment, a charge-back or a refund, gives a payment link of its own, changes a price or your currency, publishes something with a price, connects or removes a feed, or writes a rule that sends a quote or whose words name an amount or a discount, or lifts the order value you accept in person; nor does it confirm a booking or accept an order whose request held a price your catalogue does not give. Each refusal tells it to leave you a note (reply with internal: true) with what it suggests. This holds for anything that comes through the owner’s MCP, even with a full owner key, and for a key you gave another system without money:write. With returns it may open one, record a withdrawal the customer wrote, and approve a return inside your return policy with the goods coming back, while you allow it (Settings → Returns, and who you are); refusing a return, disputing what came back, cancelling a paid order, refunding without the goods and the refund itself are yours. With changes to what was agreed it does what you allow in Settings: out of the box it may accept a customer’s change to a free time before your cutoff (the cancellation window unless you set one), at your prices, and may not ask a customer for a change; a price, a longer booking or anything past the cutoff is yours.
The public server a customer’s assistant connects to at /mcp carries your business’s name, and speaks in your words and your customer’s language. When you propose another time, send a quote or suggest changes to an order, the assistant reads it with get_item_status and answers with accept_offer (only on its person’s clear yes to the terms), decline_offer, or make_offer with what they would change (suggest_time for another time); a price of their own reaches you as their message, and what you proposed still stands, unless you take customers’ own prices (Settings), when it is their answer for you to take or answer. When you ask for details, it sends them with provide_details. Before a priced booking or order binds the customer, the assistant shows them its summary and sends it again with its fingerprint on their yes; it can withdraw from a contract for them (withdraw_from_contract, the statement first) and ask to send goods back (request_return). Once a booking is confirmed or an order accepted, the same doors ask you for a change (suggest_time, make_offer) and answer a change you ask for (accept_offer, decline_offer); what was agreed stands until both say yes.