MCP Server
SuiteOp hosts a Model Context Protocol (MCP) server so AI assistants and agents can work with your SuiteOp data. Every public REST operation except the four webhook operations (the three webhook subscription operations and listWorkflowZapierSteps) is also an MCP tool: the tools are generated from the same operation catalogue as the REST API, run through the same permission checks, and return the same fields.
Endpoint
Section titled “Endpoint”Connecting an assistant with OAuth (Claude, ChatGPT and other clients that sign you in): use one address for every region:
https://mcp.suiteop.com/mcpSuiteOp routes each request to your organization’s region for you, so you don’t need to know which region you are in.
Connecting with an API key (sk_live_…): use the endpoint next to the REST API in your organization’s region. The global address only accepts OAuth tokens.
| Region | MCP endpoint for API keys |
|---|---|
| US | https://api-us.suiteop.com/mcp |
| EU | https://api-eu.suiteop.com/mcp |
| APAC | https://api-apac.suiteop.com/mcp |
How the server behaves on the wire:
- Transport: MCP Streamable HTTP, stateless. There is no session. Every request is independent and responses come back as plain JSON, not as an event stream.
- Accept: send
Accept: application/json, text/event-stream. A request that lists onlyapplication/jsonis refused with406. The official MCP SDK clients send both. - Methods: only
POST /mcp.GETandDELETEreturn405withAllow: POST. - Batches: a JSON-RPC batch may hold up to 20 messages. A larger batch is refused with
400and JSON-RPC error code-32600. - Browsers: requests that carry an
Originheader from a site SuiteOp doesn’t allow are refused with403. Server-side clients send noOriginand aren’t affected. - Server info: the server identifies itself as
suiteop(titleSuiteOp), version1.0.0, and offers tools only (no resources or prompts). Itsinitializeresponse carriesinstructions, a short description of SuiteOp that asks the model to confirm intent before calling destructive or open-world tools.
Authentication
Section titled “Authentication”/mcp accepts the same credentials as /api/v1, sent as a Bearer token:
- OAuth 2.1 (recommended for assistants that people connect themselves). When a client calls
/mcpwithout a token, the server answers401with aWWW-Authenticateheader that points to/.well-known/oauth-protected-resource. An OAuth-capable client follows it, registers itself, and walks the user through consent. The assistant then acts as that user, limited to what that user can do. See OAuth 2.1. - An API key (for your own agents and scripts). Send
Authorization: Bearer sk_live_…. The agent then acts as the key’s machine member with the key’s permissions.
Connecting a client
Section titled “Connecting a client”-
In your assistant’s settings, add a remote MCP server (sometimes called a “custom connector”) and paste
https://mcp.suiteop.com/mcp. -
The assistant opens a SuiteOp sign-in page. Sign in, pick the organization, review the requested permissions, and click Authorize.
-
The assistant lists the tools your permissions allow and can start calling them.
Clients that read a JSON configuration usually take a URL and extra headers. The exact key names vary by client, so check your client’s documentation. A typical shape:
{ "mcpServers": { "suiteop": { "type": "http", "url": "https://api-us.suiteop.com/mcp", "headers": { "Authorization": "Bearer sk_live_your_key_here" } } }}Using the official @modelcontextprotocol/sdk client:
import { Client } from '@modelcontextprotocol/sdk/client/index.js'import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
const transport = new StreamableHTTPClientTransport(new URL('https://api-us.suiteop.com/mcp'), { requestInit: { headers: { Authorization: `Bearer ${process.env.SUITEOP_API_KEY}` } },})const client = new Client({ name: 'my-agent', version: '1.0.0' })await client.connect(transport)
const { tools } = await client.listTools()const result = await client.callTool({ name: 'listTasks', arguments: { statuses: ['not_started', 'in_progress'], limit: 10 },})console.log(result.structuredContent)- One tool per REST operation, except
listWebhookSubscriptions,createWebhookSubscription,deleteWebhookSubscriptionandlistWorkflowZapierSteps, which are REST-only. The tool name is the operation ID from the API Reference, for examplelistTasks,createReservationorunlockDevice. - You only see what you may call.
tools/listincludes a tool only when the credential holds every permission the operation requires. The list for a key with onlyview_taskscontainslistDepartments,listTasks,getTask,listTaskRequirementsandlistAnswerLists, plus the four operations that need no specific permission (searchCoverIcons,searchStockPhotos,getTranslationEntries,getTranslationEntriesBatch). See the scope reference. Calling a tool that isn’t in your list returns an error resultUnknown tool: <name>. - Arguments are one flat object. Path parameters, query parameters and the request body of the REST operation are all fields of the same
argumentsobject. For example,updateTasktakes{ "id": "…", "data": { "priority": "high" } }. Each tool publishes itsinputSchema. Follow it, because for a few tools it accepts slightly more lenient input than the REST body. - Outputs have a schema. Each tool also publishes an
outputSchema. A successful call returns the result asstructuredContentand again as JSON text incontent. When an operation returns a bare list, it is wrapped as{ "data": [...] }. Paginated lists return{ "items": [...], "total": …, "limit": …, "offset": … }, the same fields REST splits intodataandmeta.pagination.
Annotations
Section titled “Annotations”Every tool carries a title (the operation’s summary without its final period, for example List tasks for the organization) and MCP annotations so a client can decide what needs a confirmation:
| Annotation | true when |
|---|---|
readOnlyHint | The operation only reads, and its REST form is a GET |
destructiveHint | The operation isn’t read-only and isn’t purely additive. This includes refreshDeviceStatus, which is a query sent as POST. Every such operation is destructive except these creates: createTask, createCode, createTranslation, create_workflow, create_email_template, addPropertyScope, createUpsell, createPrecheckStep, createElementCatalogEntry, createPropertyElement, addTemplateGroup, addTemplateChecklistItem, addTaskRequirement, createInstruction, createDepartment, createTemplate, createPropertyGroup, createPortalManual, createPortalManualCategory |
idempotentHint | The operation only reads, or is a delete. Repeating it has no further effect |
openWorldHint | The operation can reach outside your organization’s internal data: a lock or thermostat, your PMS, a guest-facing page or email, or a person by message. Among GET reads, only searchStockPhotos (a public photo search). Among everything that isn’t read-only, including refreshDeviceStatus, all except createDepartment, updateDepartment, createTemplate, updateTemplate, addTemplateGroup, updateTemplateGroup, addTemplateChecklistItem, updateTemplateChecklistItem, applyTemplateToTask, addTaskRequirement, updateTaskRequirement and create_workflow |
refreshDeviceStatus is the one query whose REST form is a POST, so it is marked idempotent but not read-only. Everything else (creates, updates, status changes, the other device commands) is non-idempotent. Those tools’ descriptions end with a retry note, because MCP has no idempotency key (see below).
Errors
Section titled “Errors”A failed tool call comes back as a normal result with isError: true and a single text block. The text starts with the same error code the REST API uses, followed by the message:
NOT_FOUND: Task not found: 7d0e…FORBIDDEN: Missing permission: modify_tasks- Validation errors start with
VALIDATION_ERROR:and list the failing fields in parentheses aspath: message, separated by semicolons, for exampleVALIDATION_ERROR: Action input validation failed (data.priority: Invalid option…). At most six fields are listed, followed by, and N morewhen there are others, and the list is cut at 400 characters. - Known business errors are rewritten into a sentence that says what to change, with the original machine key appended as
(key: …). - Provider failures start with
CONNECTOR_ERROR:. The provider’s own response is withheld. - Provider refusals start with
PROVIDER_REJECTED:. The device’s provider answered and said no. Never retry these: repeating the call sends the real device another command. Check the device in SuiteOp and report what was refused. The device command tools (lockDevice,unlockDevice,setDeviceTemperature,setDeviceMode) say this in their descriptions. - Unexpected errors return
Internal errorwith no detail.
Transport-level problems are not tool results. They are HTTP errors: 401 (missing or invalid credential), 403 (disallowed browser origin), 405 (wrong method), 400 (oversized batch) and 429 (rate limit).
Retries and idempotency
Section titled “Retries and idempotency”The REST API’s Idempotency-Key has no MCP equivalent. If a tool call’s response is lost, the call may already have been applied, and retrying a create can make a duplicate. Before repeating a non-idempotent tool call, confirm the outcome (for example with the matching list… or get… tool). Read-only tools and deletes are safe to repeat.
Rate limits
Section titled “Rate limits”MCP shares the rate limits of the credential: the same per-minute and per-second budget as REST calls made with that key or token. Each message in a JSON-RPC batch counts as one request. When the limit is hit, the server answers HTTP 429 with a Retry-After header, the usual X-RateLimit-* headers, and the body {"error":"API rate limit exceeded"}. The per-organization budget for AI-backed operations also applies, and surfaces as a tool error starting RATE_LIMITED:.