Auth & scopes
MCP accepts two credentials: a sign-in from a chat app (ChatGPT, Claude, Gemini), or the same scoped agx_ API key as the REST API. Both use the same scope model.
Signing in from a chat app
When someone connects Expedify from a chat app, they sign in with their Expedify account and approve the app on a consent screen.
- The consent screen is the API key scope picker. Every scope below is shown. Scopes the person's role doesn't allow are greyed out and can't be ticked, and nothing is ticked in advance. Select all ticks only the allowed ones.
- The app works as that person. Records are limited the same way as in the Expedify app. Someone who sees only their own contacts in Expedify sees only their own contacts through ChatGPT or Claude.
- Permissions are checked on every call against what was ticked and what the person's role allows today. A demotion, a removed permission or leaving the organisation takes effect on the next call, without reconnecting.
- The tool list is trimmed to what was ticked, so the app isn't offered tools it can't use. (For API keys and anonymous callers,
tools/liststays the full catalog; see below.) - One organisation per connection. The general address
/v1/mcpasks which organisation to connect. An organisation's own address,/v1/mcp/<org-slug>, connects that organisation directly. A sign-in for one organisation is refused on another organisation's address. - Sessions: access renews automatically every hour. A connection unused for 30 days needs a fresh sign-in.
- Ending access: Settings → Connected Apps → Revoke. Owners and admins can revoke anyone's connection in their organisation.
Owners and admins can tick every scope, which equals an API key with all scopes, but the app still acts as that one person.
The key
- Sent as
Authorization: Bearer agx_<key>onPOST /v1/mcp(bareagx_…also accepted — see Connect). - Issued per organization in Settings → API Keys. The key carries that org's tenant binding — it can never touch another org's data.
- Hash-only, shown once at creation. Rotate/revoke any time; the client is cut off instantly.
Scopes gate everything — default-deny
A key, or a sign-in, holds a set of scopes. No scope → no access at execution time. The split is:
- Discovery (
tools/list) is public — NOT scope-filtered. It returns the full static catalog — the same tools for everyone, key or no key — so an agent can see what's possible. Authenticating adds your org's exposed workflow tools; it does not remove any static tool. - Execution (
tools/call) is gated. Every call re-checks the required scope (and tenant + ACL) atoperations.execute(). A tool you hold no scope for is refused when called — a read-only key can seecreate_contactin the list but gets a scope error if it tries to call it.
Think capabilities vs. authorization: the catalog advertises everything the server can do; your key's scopes decide what it's allowed to do. Discovery is public; execution is enforced.
Scope vocabulary
Scopes are coarse {resource}:{action}. write / author / run imply read on the same resource — you don't grant the read counterpart separately.
CRM (per entity: contacts, companies, deals, tasks, notes, activities, products, deal_payments, deal_payment_schedules)
| Scope | Grants |
|---|---|
<entity>:read | View and list; search tools, get_…, list_… associations |
<entity>:write | Create / update / delete; link / unlink (implies read) |
Association tools require the scope on both sides — e.g. link_contact_to_company needs contacts:write and companies:write.
The entity-agnostic tools — search_records, get_record, create_record, update_record, delete_record, link_records, and batch_create/update/delete_records, which take the entity as an argument — use one coarse pair instead:
| Scope | Grants |
|---|---|
records:read | Search/get any CRM record of any entity type |
records:write | Create/update/delete/link any record, including batch writes (implies read) |
Workflows
| Scope | Grants |
|---|---|
workflows:read | List nodes, read/list workflows, validate, read executions |
workflows:author | Create, edit, import, activate/deactivate, delete (implies read) |
workflows:run | Run and test workflows and single nodes (implies read) |
author and run are deliberately separate, so a key can be "build but not run" or "run but not edit". Execution is additionally confirm-gated (human-in-the-loop) — see Recipes.
Knowledge Base
| Scope | Grants |
|---|---|
knowledge:read | List KBs, read documents, search and ask (RAG) |
knowledge:write | Create/update/delete KBs and documents (implies read) |
Analytics
| Scope | Grants |
|---|---|
analytics:read | List/read datasets, charts, dashboards; run queries/charts; NL→SQL; inspect schema |
analytics:write | Create/update/delete datasets, charts, dashboards (implies read) |
Channels
| Scope | Grants |
|---|---|
templates:read / templates:write | Marketing templates — list/read; create, update, delete |
assets:read / assets:write | Marketing assets — list/read; upload, update, delete |
segments:read / segments:write | Segments — list/read, evaluate & preview audiences; create, update, delete |
campaigns:read / campaigns:write | Campaigns — list/read; create, update, cancel |
campaigns:send | Launch a campaign — real, mass, billable outbound (implies read) |
communications:read | Read voice calls, WhatsApp conversations & messages, SMS logs |
campaigns:send is separate from campaigns:write so an agent can draft campaigns it can't fire. Sends are additionally confirm-gated.
Settings
| Scope | Grants |
|---|---|
integrations:read | Browse the catalog; list/read connected integrations (never exposes credentials) |
integrations:write | Create an integration and run live connection tests — a real outbound call with stored credentials (implies read) |
webhooks:read / webhooks:write | Incoming + outgoing webhooks, logs, event types; create/update/delete/test |
Automation (Blueprints)
| Scope | Grants |
|---|---|
blueprints:read | Read/list a workflow's visual Blueprint and validate it |
blueprints:write | Create, replace, and clear a Blueprint (implies read) |
Search
| Scope | Grants |
|---|---|
search:read | Cross-entity find-by-name over workflows, charts, dashboards, and datasets |
Mapping a tool to its scope
Every tool in the catalog lists its Required scope. Rule of thumb:
search_… / get_… / list_…→…:readcreate_… / update_… / delete_… / link_… / run_… / edit_…→ the resource'swrite/author/run
Grant the narrowest set that gets the job done. A reporting agent needs only …:read scopes; a data-entry agent adds the relevant …:write. This is the same posture the REST API enforces — the Scopes reference is shared between both surfaces.