MCP
Keystone MCP server
Nine tools: five the model calls to read a business and stage changes, four the Keystone card calls on a click to claim, open, apply, and undo.
The server
Endpoint https://mcp.keystone.app/mcp. Streamable HTTP, stateless, serving the 2026-07-28 protocol and 2025-era clients; any client that speaks MCP connects the same way (Connecting an MCP client). Clients authenticate with an OAuth access token issued by the Auth service; the server forwards every call to the Edits API with that token and its own service key, and holds no data of its own.
The model never changes anything directly. It proposes; the user reviews each change on a card that names the business and applies with a click the model cannot make. SOR enforces who may edit which business, compare-and-swap on every field, and keeps the change log the console shows under Change History.
find_business Find a business
Find the business the user wants to work on, by name, website or domain. Shows a picker card: the user clicks the business, which opens an edit session, and its session id comes back to the model. The model cannot open a session or claim a business itself. A business can be edited while the user holds it (a claim, for 24 hours) and its site is not live; one nobody holds they can claim with a click on the card.
Input
| Field | Type | Description |
|---|---|---|
queryrequired | string (2–200) | A business name, website or domain. |
Returns
Text listing each match as editable or the reason it is locked, and structuredContent.view = "picker" with the matches. _meta.cardTokens carries one token per click the card may make (open an editable business, claim an unclaimed one).
Edits API calls
get_snapshot Business snapshot
Everything about the business in an edit session: the profile in full and every record (locations, services, service items, packages, team, FAQs, job postings, offers) with its id, label and version. Start here, before proposing changes. The user sees a card of what needs work. Hours live on the main location and are read separately so the card can show them.
Input
| Field | Type | Description |
|---|---|---|
session_idrequired | uuid | The edit session id the user opened (returned when they pick a business on the card). |
Returns
The snapshot as JSON text, and structuredContent.view = "snapshot" with the business, summary rows for the card, and a console URL.
Edits API calls
read_business Read a record
One record in full, with its write field names and version: the profile (entity "profile", record_id "profile") or any record get_snapshot lists.
Input
| Field | Type | Description |
|---|---|---|
session_idrequired | uuid | The edit session id the user opened (returned when they pick a business on the card). |
entityrequired | string | The entity, as get_snapshot names it. |
record_idrequired | string | The record id, or "profile" for the profile. |
Returns
The record as JSON text.
Edits API calls
propose_changes Propose changes
Stage changes to the business for the user to review. Nothing is saved: they see each change on a card that names the business, untick any they do not want, and click Apply. Send every change in one call, up to 50. Include expected (the values read) or version, so anything changed since is caught. Changes that look like they came from another of the user's businesses start unticked. The call is idempotent on its content within a session.
Input
| Field | Type | Description |
|---|---|---|
session_idrequired | uuid | The edit session id the user opened (returned when they pick a business on the card). |
summary | string (≤500) | One line saying what these changes do. |
itemsrequired | ChangeItem[] (1–50) | The changes; see ChangeItem below. |
Returns
Text naming the staged changeset and each change, with a console link for applying without the card; structuredContent.view = "changeset"; _meta.cardTokens.apply for the card's Apply click. Changes SOR refuses are listed by position with a reason so the model can fix only those.
Edits API calls
discard_changeset Discard a draft
Drop a staged changeset the user no longer wants. Nothing was saved from it.
Input
| Field | Type | Description |
|---|---|---|
changeset_idrequired | uuid | The changeset id propose_changes returned. |
Returns
Confirmation text.
Edits API calls
claim_business Claim this business
Called by the Keystone card when the user clicks Claim on the business picker. Claims an unclaimed, unlaunched business for the caller for 24 hours so nobody else can take it meanwhile.
Input
| Field | Type | Description |
|---|---|---|
business_idrequired | uuid | The business id, as find_business lists it. |
card_tokenrequired | string | The token the card was given for this exact action and target. Minted per click; the model never has one. |
Returns
Text with the claim's expiry; structuredContent.view = "claimed"; a fresh open:<business_id> card token for the next click.
Edits API calls
open_session Work on this business
Called by the Keystone card when the user picks a business. Binds the chat to that business by opening an edit session; the model then uses the session id for get_snapshot and propose_changes.
Input
| Field | Type | Description |
|---|---|---|
business_idrequired | uuid | The business id, as find_business lists it. |
card_tokenrequired | string | The token the card was given for this exact action and target. Minted per click; the model never has one. |
Returns
Text naming the business, the session id and its expiry; structuredContent.view = "session".
Edits API calls
apply_changeset Apply changes
Called by the Keystone card when the user clicks Apply. Applies the ticked items (or exactly the ones given). Each applied change lands in the console's Change History.
Input
| Field | Type | Description |
|---|---|---|
changeset_idrequired | uuid | The changeset id propose_changes returned. |
item_ids | uuid[] (≤50) | Exactly these items; omitted, the ticked ones. |
card_tokenrequired | string | The token the card was given for this exact action and target. Minted per click; the model never has one. |
Returns
Text with a tally (applied, skipped, refused) and each change; structuredContent.view = "changeset"; an undo card token.
Edits API calls
undo_changeset Undo changes
Called by the Keystone card when the user clicks Undo. Reverts an applied changeset. A value someone changed again since the apply is kept, so an undo can undo nothing; the text says so.
Input
| Field | Type | Description |
|---|---|---|
changeset_idrequired | uuid | The changeset id propose_changes returned. |
card_tokenrequired | string | The token the card was given for this exact action and target. Minted per click; the model never has one. |
Returns
Text with what was reverted and what was kept; structuredContent.view = "changeset".
Edits API calls
ChangeItem
One change inside propose_changes. What the model may send is exactly what the Edits API accepts; a change the contract no longer takes fails at the tool boundary with a reason.
| Field | Type | Description |
|---|---|---|
entityrequired | string | The entity, as get_snapshot names it: profile, location, service, service_item, package, team_member, faq, job_posting, offer. |
oprequired | "create" | "update" | "delete" | What to do. |
record_id | string | The record to update or delete; omit for the profile and for creates. |
fields | object | New values by write field name; a nested object may be sent in part. |
expected | object | The values you read for those fields, so a change made since is caught (compare-and-swap). |
version | string | The record version you read, from get_snapshot or read_business. An alternative to expected. |