Connecting to the MCP server
Point any MCP client or AI agent at Zoop with one endpoint — POST /api/mcp, JSON-RPC 2.0, Bearer auth.
This page explains how to connect an AI agent or MCP client to your Zoop account and start calling tools.
Zoop exposes a Model Context Protocol (MCP) server so AI agents and MCP-capable clients — Claude Desktop, LangGraph, n8n, and others — can read and write your Zoop data through a stable tool catalog. This is the same tool layer the in-product Zoop AI uses, now available to external integrations.
What is MCP?
MCP (Model Context Protocol) is an open standard that lets AI agents call structured tools over HTTP. Instead of scraping a UI or parsing raw API responses, an MCP client asks the server for a list of available tools, then calls them by name with typed inputs. The server handles authentication, validation, and execution.
The endpoint
All MCP calls go to one URL:
POST /api/mcp
The base URL depends on your environment. Do not hardcode a hostname from this page. Look up the correct base URL for your environment (production, staging, or local) from the /.well-known/oauth-protected-resource discovery document. When in doubt, start there.
A few things worth knowing up front:
- Single endpoint, no tenant prefix. There is no per-tenant path like
/tenantId/api/mcp. Your access token tells Zoop which tenant you are — so a token can only ever operate on the one tenant it was issued for. - Wire format: Zoop's MCP server uses streamable HTTP transport with JSON-RPC 2.0 messages. If you are using an MCP client library, it handles this for you automatically.
- Server identity: the server identifies itself as
zoopand supportstools,resources, andprompts.
Authentication
Every request to /api/mcp must include an Authorization header with a token. Think of the token as the key that proves who you are and which Zoop account you can access.
Authorization: Bearer <token>
There are two kinds of token:
API keys are the simplest option for server-to-server integrations. Generate one in Zoop and pass it directly — no login flow required.
POST /api/mcp
Authorization: Bearer zoop_uk_••••••••
Content-Type: application/json
{ ...json-rpc payload... }
Zoop issues two kinds of API key, which you can tell apart by their prefix:
zoop_uk_— a user key, tied to a specific user and tenant. Use this when your tools need to record who created or updated something.zoop_tk_— a tenant key, a service credential with no user identity. It can call read tools but cannot call write tools that require a user (see User-actor requirement below).
See API keys for how to create and manage keys.
Use OAuth 2.0 when your integration needs to act on behalf of a user who logs in through a browser flow.
POST /api/mcp
Authorization: Bearer eyJ...
Content-Type: application/json
{ ...json-rpc payload... }
MCP clients that support auto-discovery should fetch /.well-known/oauth-protected-resource to find the authorization server. The resource parameter is required in both the authorization request and the token request — set it to the resource value from that discovery document.
See OAuth for the full flow.
If authentication fails, the endpoint returns a structured error response (not a bare HTTP 401 page) plus a WWW-Authenticate header pointing at the discovery document, so a compliant MCP client can auto-discover the authorization server.
User-actor requirement
Some write tools require a user actor — meaning they record which specific user performed the action by stamping created_by, updated_by, and assignment fields with a real user ID. A tenant-key token (zoop_tk_) has no user ID attached, so it cannot call those tools. Attempting to do so returns an invalid_input error. Read tools work with both token kinds.
Tools that require a user actor are marked U in the tool catalog below.
Scopes and authorization
A scope is a permission string that controls what a token is allowed to do. Each tool requires at least one scope, and your token must include that scope or the call is denied before execution with an insufficient_scope error naming the missing scope.
Scopes follow a read:<entity> / write:<entity> shape. write:<entity> is not a superset of read:<entity> — if you need to both read and write the same entity, grant both scopes.
Two umbrella scopes expand at verification time: read:catalog expands to read:catalog_items + read:catalog_categories (and the same for write:). read:estimates and write:estimates are legacy aliases for quotes — prefer read:quotes / write:quotes for new integrations.
See Scopes for the full scope catalog.
Connect an MCP client
Most MCP clients accept a server config block that tells them where the server is and how to authenticate. Here is the minimal config you need:
{
"mcpServers": {
"zoop": {
"type": "http",
"url": "https://<your-zoop-base-url>/api/mcp",
"headers": {
"Authorization": "Bearer <your-api-key-or-token>"
}
}
}
}
{
"mcpServers": {
"zoop": {
"url": "https://<your-zoop-base-url>/api/mcp",
"headers": {
"Authorization": "Bearer <your-api-key-or-token>"
}
}
}
}
Both blocks give the client a url and an Authorization header. Zoop's MCP server speaks streamable HTTP natively — you do not need a local proxy process (such as mcp-remote) to connect.
Replace <your-zoop-base-url> with the base URL for your environment (look it up from /.well-known/oauth-protected-resource). Replace <your-api-key-or-token> with a zoop_uk_ or zoop_tk_ key from API keys.
Making requests directly with curl
If you want to call the endpoint yourself without an MCP client library, use curl. The examples below show the exact request format.
List available tools
curl -X POST https://<your-zoop-base-url>/api/mcp \
-H "Authorization: Bearer zoop_uk_••••••••" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}'
The response lists every available tool with its name, description, and the fields it accepts:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "customers.search",
"description": "Search customers by name, phone, tag, or status.",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string" },
"limit": { "type": "integer" }
}
}
}
]
}
}
Call a specific tool
curl -X POST https://<your-zoop-base-url>/api/mcp \
-H "Authorization: Bearer zoop_uk_••••••••" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "customers.search",
"arguments": {
"query": "Sarah",
"limit": 5
}
}
}'
A successful response looks like this:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "text",
"text": "{ "customers": [...], "hasMore": false }"
}
],
"isError": false
}
}
If the tool call fails — for example because your token is missing a scope — you still get an HTTP 200, but isError is true:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "text",
"text": "insufficient_scope: token is missing read:customers"
}
],
"isError": true
}
}
Per the MCP spec, tool errors always return HTTP 200 with isError: true in the result body. Only transport-level and auth failures return non-200 HTTP status codes (see Error reference below).
Error reference
Tool errors appear in the response body with isError: true. The text field contains the error code and a short message.
| Error code | What it means | Common cause |
|---|---|---|
invalid_input | Input failed validation, or a precondition was not met | Wrong field type, missing required field, or a tenant-key token calling a user-only tool |
not_found | The record does not exist in this tenant (or is archived) | Stale ID, cross-tenant ID, or already-archived target |
conflict | The request collides with current state | Duplicate name, illegal status transition, or editing a non-draft invoice |
insufficient_scope | Your token is missing a required scope | Add the missing read: or write: scope to your token |
internal | Unexpected server error | No details are exposed to the client |
Auth failures happen before any tool runs and use standard HTTP status codes: 401 for invalid, expired, or revoked tokens; 403 for wrong origin; 429 for rate limiting. Rate-limited responses include a Retry-After header.
Tool catalog
There are 65 tools across 13 entities. Every entity follows a five-verb pattern: list or search, get, create, update, and a destructive verb (usually archive, but some entities diverge — see the notes under each section).
Tool names follow the <entity>.<verb> format, for example customers.search or job_series.list. U marks tools that require a user actor — tenant-key tokens (zoop_tk_) cannot call them.
Customers — read:customers / write:customers
See Customers API.
| Tool | Scope | What it does |
|---|---|---|
customers.search | read:customers | Search by name, phone, tag, or status. Returns rows and a pagination cursor. |
customers.get | read:customers | Fetch one customer's full record including contacts and locations. |
customers.create | write:customers | Create a customer (individual or company). |
customers.update | write:customers | Update mutable fields. Cannot change customer_type. |
customers.archive | write:customers | Soft-archive a customer. |
Contacts — read:customers / write:customers
Contacts are a sub-resource of customers and share the customer scopes.
| Tool | Scope | What it does |
|---|---|---|
contacts.list | read:customers | List a customer's contacts (primary first). Requires customer_id. |
contacts.get | read:customers | Fetch one contact by ID. |
contacts.create | write:customers | Add a contact. The first contact becomes primary. Phone is normalized to E.164. |
contacts.update | write:customers | Partial patch. Cannot move a contact between customers. |
contacts.archive | write:customers | Soft-archive a contact. |
Locations — read:customers / write:customers
Locations are a sub-resource of customers and share the customer scopes.
| Tool | Scope | What it does |
|---|---|---|
locations.list | read:customers | List a customer's service locations (primary first). Requires customer_id. |
locations.get | read:customers | Fetch one location by ID. |
locations.create | write:customers | Add a service location. The first per type becomes primary. |
locations.update | write:customers | Partial patch. Cannot move a location between customers. |
locations.archive | write:customers | Soft-archive a location. |
Tags — read:customers / write:customers
Tags are account-level labels that share the customer scopes.
tags.delete is a hard delete — it removes the tag and all its assignments permanently. There is no way to undo this.
| Tool | Scope | What it does |
|---|---|---|
tags.list | read:customers | List the tenant's tag library, ordered by name. |
tags.get | read:customers | Fetch one tag by ID. |
tags.create | write:customers | U Create a tag with an optional #RRGGBB color. |
tags.update | write:customers | Rename or recolor a tag. |
tags.delete | write:customers | Hard delete — removes the tag and all assignments. |
Notes — read:notes / write:notes
See Notes API.
| Tool | Scope | What it does |
|---|---|---|
notes.list | read:notes | List notes (newest first). Filter by pinned, subject, full-text search, or date range. |
notes.get | read:notes | Fetch one note by ID. |
notes.create | write:notes | U Create a note with an optional subject attachment. |
notes.update | write:notes | U Partial patch. Author or owner only. |
notes.archive | write:notes | U Soft-archive a note. Author or owner only. |
Jobs — read:jobs / write:jobs
See Jobs API. The destructive verb for jobs is cancel (not archive) because jobs use a status machine.
| Tool | Scope | What it does |
|---|---|---|
jobs.list | read:jobs | List jobs filtered by status, customer, series, date range, or title. |
jobs.get | read:jobs | Fetch one job including line items, assignments, and media. |
jobs.create | write:jobs | U Create a job. Providing scheduled_start sets status to scheduled. |
jobs.update | write:jobs | U Update mutable fields. Setting status="done" stamps completed_at. |
jobs.cancel | write:jobs | U Cancel a job. One-way status transition; history is preserved. |
Job series — read:job_series / write:job_series
See Job series API. The destructive verb is end — it closes the series and cancels all future instances.
| Tool | Scope | What it does |
|---|---|---|
job_series.list | read:job_series | List recurring series templates. Use jobs.list with a seriesId filter to see individual instances. |
job_series.get | read:job_series | Fetch one series including template line items and assignments. |
job_series.create | write:job_series | U Create a series. Accepts an RFC 5545 rrule recurrence string, an IANA timezone name, and a dtstart start date. The first job window is generated immediately. |
job_series.update | write:job_series | U Updates apply to all future instances — future jobs are regenerated; completed and cancelled jobs are left intact. |
job_series.end | write:job_series | End the series. Caps the end date and cancels future instances. One-way. |
Invoices — read:invoices / write:invoices
See Invoices API. The destructive verb is void — it marks the invoice as void and preserves the billing history. Voiding cannot be undone.
| Tool | Scope | What it does |
|---|---|---|
invoices.list | read:invoices | List invoices (newest first). Voided invoices are excluded unless a status filter is given. |
invoices.get | read:invoices | Fetch one invoice with fields and computed totals. |
invoices.create | write:invoices | U Create a draft invoice. Totals and tax are computed server-side. |
invoices.update | write:invoices | U Status transitions are allowed at any time; other fields are editable only while in draft. Line items replace wholesale. |
invoices.void | write:invoices | Void an invoice. One-way. |
Quotes — read:quotes / write:quotes
See Quotes API. Write tools for quotes are owner-gated — even a token with write:quotes is denied if it is not the owner of the quote.
| Tool | Scope | What it does |
|---|---|---|
quotes.list | read:quotes | List quotes (newest first). Archived quotes excluded unless a status filter is given. |
quotes.get | read:quotes | Fetch one quote. |
quotes.create | write:quotes | U Create a draft quote. Owner-gated. |
quotes.update | write:quotes | U Update shell fields. Illegal status transitions return a conflict error. Owner-gated. |
quotes.archive | write:quotes | U Archive a quote. Owner-gated. |
Catalog items — read:catalog_items / write:catalog_items
See Catalog items API. Cost and supplier fields are only visible to the owner — they are redacted for other users and for tenant-key tokens.
| Tool | Scope | What it does |
|---|---|---|
catalog_items.list | read:catalog_items | List catalog items across all kinds: service, product, labor, fee, bundle, discount. |
catalog_items.get | read:catalog_items | Fetch one item. Bundle components are included. |
catalog_items.create | write:catalog_items | U Create an item. The kind field selects the variant. |
catalog_items.update | write:catalog_items | U Partial patch. Passing components replaces a bundle's component list atomically. |
catalog_items.archive | write:catalog_items | U Soft-archive a catalog item. |
Catalog categories — read:catalog_categories / write:catalog_categories
catalog_categories.delete is a hard delete — it moves child categories up to the parent, clears the category_id on any items in the category, and removes the row permanently.
| Tool | Scope | What it does |
|---|---|---|
catalog_categories.list | read:catalog_categories | List categories as flat rows with parent_id for client-side tree building. |
catalog_categories.get | read:catalog_categories | Fetch one category by ID. |
catalog_categories.create | write:catalog_categories | U Create a category. Name must be unique within the parent. |
catalog_categories.update | write:catalog_categories | U Partial patch. Cycle re-parenting is a conflict error. |
catalog_categories.delete | write:catalog_categories | Hard delete — re-parents children, clears category references, removes the row. |
Plans — read:plans / write:plans
The destructive verb is cancel — cancellation is a terminal state. A cancelled plan cannot be reactivated.
| Tool | Scope | What it does |
|---|---|---|
plans.list | read:plans | List recurring plans (newest first). Filter by status. |
plans.get | read:plans | Fetch one plan including line items in sort order. |
plans.create | write:plans | U Create a plan template with a recurrence cadence and at least one line item. This defines the plan only — it does not charge the customer. |
plans.update | write:plans | U Partial patch. Items replace wholesale. Customer is immutable. |
plans.cancel | write:plans | U Cancel a plan. Terminal — no resume, no refund. |
Tax rates — read:tax_rates / write:tax_rates
| Tool | Scope | What it does |
|---|---|---|
tax_rates.list | read:tax_rates | List tax rates including the tenant default. Pass include_archived to see archived rates. |
tax_rates.get | read:tax_rates | Fetch one rate. The rate_decimal field stores the decimal form (for example 0.0825 = 8.25%). |
tax_rates.create | write:tax_rates | Create a rate. Pass rate_percentage as a number like 8.25. Set is_default: true to make it the account default. |
tax_rates.update | write:tax_rates | Partial patch. is_default toggles the tenant default. |
tax_rates.archive | write:tax_rates | Soft-archive a rate. Clears the default if this was the tenant default. |
Pagination
Two pagination styles are used across the tool catalog:
- Cursor-based (
customers.search,notes.list): pass thecursorvalue from the previous response to fetch the next page. CheckhasMoreto know if there are more results, and readnextCursorto get the value to pass next time. - Page-based (all other list tools): pass a
pageinteger starting at1.
What is not available yet
The following are intentionally out of scope for the current version:
- Payments. There are no payment tools. Payments, charges, Stripe state, and refunds are planned for a future release.
- Specialized actions. Operations beyond core create/read/update/archive — for example
notes.pin, quote PDF generation, invoice send, plan charge-now, tag attach/detach, and customer merge — are not yet exposed as MCP tools. - Account administration. Tenant settings, billing, and credential administration are not exposed over MCP.
- Scopes without tools. The
communications,companies,settings, andagent_sessionsscopes exist for other Zoop surfaces and do not yet back any MCP tools.