AI and MCP
MCP server
Connect Claude, ChatGPT, Cursor, VS Code or any Model Context Protocol client to your helpdesk. Search, read, reply to and route tickets in natural language, with scoped access that acts with your own permissions. Sign in with OAuth or use a token.
Endpoint: https://zono.support/mcp
Endpoint
The server speaks the MCP streamable HTTP transport at a single URL. Clients that support OAuth need nothing else; others send a token in the Authorization header.
https://zono.support/mcpConnect with OAuth
The recommended way. Your client registers itself, you sign in to Zono in the browser, pick the workspace and the scopes to grant, and approve. No token to copy or rotate.
- Claude and ChatGPT: add a custom connector (in Claude: Settings > Connectors; in ChatGPT: Settings > Connectors) and paste
https://zono.support/mcp. Connect, sign in, approve. - Cursor and VS Code: configure only the URL (see Client setup). The consent page opens the first time the client connects.
- Claude Code: add the server by URL, then run
/mcpand choose Authenticate.
The consent page names the app as unverified (it names itself when it registers) and shows where it will send you back, so only approve apps you are connecting yourself. Clients must use PKCE with S256, and redirect URIs must match the registered ones exactly. Discovery documents are at /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server.
Bearer tokens
For clients without OAuth, scripts and CI, send a personal access token: Authorization: Bearer <token>. Tokens are created under Account > API & MCP tokens, are scoped to one workspace, carry the scopes you tick, and are shown once. A token expires after 90 days unless you pick another lifetime (7, 30 or 365 days, or never), and we email you each time one is created. You can also give it an IP allowlist; requests from other addresses are refused. The same token also works for the REST API.
New to Zono? Start free, then create a token.
Scopes
Every tool needs one scope. OAuth connections carry the scopes approved on the consent page; tokens carry the ones ticked when they were created (the Read only and Read and write presets tick the read scopes, or all of them). Your client still lists every tool: a call outside the token's scopes returns an error result naming the missing scope, and nothing changes. Tokens made before scopes existed keep working as before: read-only tokens can call the read tools, read-write tokens every tool.
| Scope | Allows | Tools |
|---|---|---|
| tickets:read | List, search and read tickets, their replies, notes and custom fields. | list-tickets, get-ticket, search-tickets, list-ticket-fields |
| tickets:write | Create tickets, reply, add notes, assign, change status or fields, and merge tickets. | reply-to-ticket, assign-ticket, update-ticket-status, update-ticket-fields, create-ticket, merge-tickets |
| customers:read | List and read customers (team owners and admins only). | list-customers |
| knowledge:read | Search the help center and knowledge sources, and ask for cited answers. | search-knowledge, ask-knowledge |
| licenses:read | Look up and verify license keys and purchases. | lookup-license |
REST API only, no MCP tool (OAuth apps that ask for them, such as automation tools, are offered them): customers:write (change customers), conversations:read (read conversations), conversations:write (change conversations), webhooks:manage (manage webhooks).
Lifetimes and revocation
OAuth access tokens last 60 minutes and clients renew them with a refresh token, which is replaced on every use and expires after 180 days unused. Authorization codes last 10 minutes and work once. Disconnect an app under Account > API & MCP tokens > Connected apps (team owners and admins can also do it from Settings > API tokens): its access and refresh tokens stop working at once. Leaving a workspace cuts off access too. Personal access tokens last until they expire or are revoked.
Tools
Your client discovers these automatically. Parameters marked * are required.
list-ticketsReadtickets:readList tickets in the current team that the agent can access, optionally filtered by status, assignee, priority, department or custom field values.
| Parameter | Type | Description |
|---|---|---|
| status | string | Filter by ticket status: open, answered, on-hold, or closed. |
| assignee_id | string | Filter to tickets assigned to a specific user ULID. |
| department_id | string | Filter to tickets in a specific department ULID. |
| priority | string | Filter by priority: low, normal, high, or urgent. |
| limit | integer | Maximum number of tickets to return (default 25, max 100). |
| custom_fields | object | Filter by custom field values keyed by field key: a value (or list of option values) to match, {"min","max"} for numbers, {"from","to"} for dates, "__empty__" for no value or "__any__" for any value. |
get-ticketReadtickets:readFetch the full conversation thread for a single ticket by ID or number, including all replies, internal notes, SLA status and custom field values.
| Parameter | Type | Description |
|---|---|---|
| ticket* | string | Ticket ULID or ticket number. |
reply-to-ticketWritetickets:writePost a reply to a ticket as the authenticated agent. Set is_internal=true for staff-only notes.
| Parameter | Type | Description |
|---|---|---|
| ticket* | string | Ticket ULID or ticket number to reply to. |
| message* | string | Reply body (plain text or HTML). HTML is preserved. |
| is_internal | boolean | When true, the reply becomes a staff-only internal note (default: false). |
| close_ticket | boolean | When true, close the ticket after posting the reply (default: false). |
assign-ticketWritetickets:writeAssign a ticket to a team member, or unassign it by passing assignee_id=null.
| Parameter | Type | Description |
|---|---|---|
| ticket* | string | Ticket ULID or ticket number. |
| assignee_id | string | User ULID to assign the ticket to. Omit or send null to unassign. |
update-ticket-statusWritetickets:writeChange a ticket status (open, answered, on-hold, closed). Closing needs every required custom field filled in; pass missing values in custom_fields, keyed by field key.
| Parameter | Type | Description |
|---|---|---|
| ticket* | string | Ticket ULID or ticket number. |
| status* | string | New ticket status: open, answered, on-hold, or closed. |
| custom_fields | object | Optional custom field values to save with the status change, keyed by field key, e.g. {"order_number": "1001"}. Use list-ticket-fields for keys, types and options. |
update-ticket-fieldsWritetickets:writeSet custom field values on an existing ticket. Values are keyed by field key (see list-ticket-fields); null or an empty value clears an optional field.
| Parameter | Type | Description |
|---|---|---|
| ticket* | string | Ticket ULID or ticket number. |
| custom_fields* | object | Custom field values keyed by field key, e.g. {"order_number": "1001", "category": "bug"}. Multi-select values are arrays of option values, dates are YYYY-MM-DD. Only fields that apply to the ticket's department and product are accepted. |
create-ticketWritetickets:writeCreate a new ticket on behalf of a customer. The customer and product must already exist in the team. Custom field values are keyed by field key (see list-ticket-fields).
| Parameter | Type | Description |
|---|---|---|
| customer_id* | string | Customer ULID the ticket is created for. |
| subject* | string | Ticket subject line. |
| description* | string | Initial ticket body / description. |
| product_id* | integer | Product ID the ticket relates to. |
| priority | string | Optional priority: low, normal, high, urgent. |
| department_id | string | Optional department ULID to route the ticket to. |
| custom_fields | object | Optional custom field values keyed by field key, e.g. {"order_number": "1001", "category": "bug"}. Use list-ticket-fields for keys, types and options; multi-select values are arrays of option values, dates are YYYY-MM-DD. |
search-ticketsReadtickets:readFull-text search across ticket subject, description, and customer name. Honors per-agent access rules.
| Parameter | Type | Description |
|---|---|---|
| query* | string | Search term to match against subject, description, customer name, and customer email. |
| status | string | Optional status filter. |
| limit | integer | Maximum results to return (default 20, max 50). |
list-customersReadcustomers:readList or search customers belonging to the current team. Team admins and owners only, as in the dashboard.
| Parameter | Type | Description |
|---|---|---|
| search | string | Optional substring to match against customer name or email (case-insensitive; % and _ match literally). |
| limit | integer | Maximum customers to return (default 25, max 100). |
search-knowledgeReadknowledge:readSearch the team knowledge (help articles, saved replies, snippets, resolved tickets, websites, files) with the team's AI Knowledge settings. Returns ranked passages with scores and citations.
| Parameter | Type | Description |
|---|---|---|
| query* | string | What to look up. |
| ticket | string | Optional ticket ID or number to search in the context of (its product, department and channel). |
| product_id | string | Optional product ID to scope the search to. |
| limit | integer | Maximum passages to return (1-20, default from settings). |
ask-knowledgeReadknowledge:readAsk a question and get an answer grounded in the team knowledge, with [S#] citations, a confidence score and whether a human should handle it.
| Parameter | Type | Description |
|---|---|---|
| query* | string | The question to answer. |
| ticket | string | Optional ticket ID or number to answer in the context of. |
| product_id | string | Optional product ID to scope the answer to. |
list-ticket-fieldsReadtickets:readList the team's active custom ticket fields with their keys, types, options and whether they are required, for use with create-ticket and list-tickets.
No parameters.
merge-ticketsWritetickets:writeMerge a duplicate ticket (source) into another ticket (target) of the same customer. The target receives the source's replies, notes, attachments and tags; the source is closed and linked to the target, and future customer messages to it land on the target. Tickets of different customers cannot be merged.
| Parameter | Type | Description |
|---|---|---|
| source* | string | Ticket ULID or number of the duplicate ticket to merge (it will be closed). |
| target* | string | Ticket ULID or number of the ticket to merge into (it stays open). Must belong to the same customer as the source. |
| notify_customer | boolean | Tell the source ticket's customer where their request went. Defaults to false. |
| note | string | Optional internal note added to both tickets. |
lookup-licenseReadlicenses:readLook up licenses and purchases: pass a ticket (ID or number) for its requester's storefront card (stored licenses, purchases found by email and a refund flag), or a license_key (team admins and owners only) for the stored license with that key and its customer. Keys come back masked to their last four characters.
| Parameter | Type | Description |
|---|---|---|
| ticket | string | Ticket ULID or ticket number: returns its requester's licenses and purchases. Pass this or license_key, not both. |
| license_key | string | A license key or purchase code to find among the team's stored licenses (team admins and owners only). Pass this or ticket, not both. |
Permission model
Tools run as the user who connected the app or issued the token, scoped to one workspace. They inherit that user's role and department and product access, so the AI sees and changes only the tickets you could in the dashboard; scopes narrow that further, never widen it. Customer listing is limited to team owners and admins, as in the dashboard. Leaving the workspace, disconnecting the app or revoking the token cuts off access immediately.
Rate limits
60 requests per minute per token (or per connected app) and 300 per workspace; the tighter limit applies. The per-workspace limit stops many tokens on one workspace from crowding out others. Over the limit, requests get a 429 with Retry-After.
Audit trail
Every tool call is logged after it runs, with the tool, the user, the token or connected app, its outcome (including calls refused for a missing scope) and its latency. Creating and revoking tokens, and connecting and disconnecting apps, are logged too. Team owners see these entries in the workspace activity log.
Client setup
OAuth first; with a bearer token, replace <YOUR_TOKEN> with your token.
Claude Code
Run this once in your terminal, then type /mcp in Claude Code and choose Authenticate: your browser opens the Zono sign-in and consent page. With a token instead, add the Authorization header.
claude mcp add --transport http zono-support https://zono.support/mcpClaude Desktop
Easiest: Settings > Connectors > Add custom connector, then paste the endpoint. To configure it by file instead, Claude Desktop starts local servers from claude_desktop_config.json, so it reaches Zono through the mcp-remote bridge (needs Node.js for npx); mcp-remote opens the consent page itself. The bearer variant passes the header through an env var because some clients do not escape spaces in args.
{
"mcpServers": {
"zono-support": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://zono.support/mcp"
]
}
}
}Cursor
Add the server to ~/.cursor/mcp.json (or .cursor/mcp.json in a project) with only the URL. The first time it connects, Cursor asks you to log in and opens the consent page. With a token instead, add the headers block.
{
"mcpServers": {
"zono-support": {
"url": "https://zono.support/mcp"
}
}
}VS Code
Add the server to .vscode/mcp.json (or your user settings) with only the URL. VS Code asks you to sign in when it first starts the server.
{
"servers": {
"zono-support": {
"type": "http",
"url": "https://zono.support/mcp"
}
}
}