ConceptsConnecting to the MCP server

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 zoop and supports tools, resources, and prompts.

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.

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>"
      }
    }
  }
}

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 codeWhat it meansCommon cause
invalid_inputInput failed validation, or a precondition was not metWrong field type, missing required field, or a tenant-key token calling a user-only tool
not_foundThe record does not exist in this tenant (or is archived)Stale ID, cross-tenant ID, or already-archived target
conflictThe request collides with current stateDuplicate name, illegal status transition, or editing a non-draft invoice
insufficient_scopeYour token is missing a required scopeAdd the missing read: or write: scope to your token
internalUnexpected server errorNo 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.

ToolScopeWhat it does
customers.searchread:customersSearch by name, phone, tag, or status. Returns rows and a pagination cursor.
customers.getread:customersFetch one customer's full record including contacts and locations.
customers.createwrite:customersCreate a customer (individual or company).
customers.updatewrite:customersUpdate mutable fields. Cannot change customer_type.
customers.archivewrite:customersSoft-archive a customer.

Contacts — read:customers / write:customers

Contacts are a sub-resource of customers and share the customer scopes.

ToolScopeWhat it does
contacts.listread:customersList a customer's contacts (primary first). Requires customer_id.
contacts.getread:customersFetch one contact by ID.
contacts.createwrite:customersAdd a contact. The first contact becomes primary. Phone is normalized to E.164.
contacts.updatewrite:customersPartial patch. Cannot move a contact between customers.
contacts.archivewrite:customersSoft-archive a contact.

Locations — read:customers / write:customers

Locations are a sub-resource of customers and share the customer scopes.

ToolScopeWhat it does
locations.listread:customersList a customer's service locations (primary first). Requires customer_id.
locations.getread:customersFetch one location by ID.
locations.createwrite:customersAdd a service location. The first per type becomes primary.
locations.updatewrite:customersPartial patch. Cannot move a location between customers.
locations.archivewrite:customersSoft-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.

ToolScopeWhat it does
tags.listread:customersList the tenant's tag library, ordered by name.
tags.getread:customersFetch one tag by ID.
tags.createwrite:customersU Create a tag with an optional #RRGGBB color.
tags.updatewrite:customersRename or recolor a tag.
tags.deletewrite:customersHard delete — removes the tag and all assignments.

Notes — read:notes / write:notes

See Notes API.

ToolScopeWhat it does
notes.listread:notesList notes (newest first). Filter by pinned, subject, full-text search, or date range.
notes.getread:notesFetch one note by ID.
notes.createwrite:notesU Create a note with an optional subject attachment.
notes.updatewrite:notesU Partial patch. Author or owner only.
notes.archivewrite:notesU 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.

ToolScopeWhat it does
jobs.listread:jobsList jobs filtered by status, customer, series, date range, or title.
jobs.getread:jobsFetch one job including line items, assignments, and media.
jobs.createwrite:jobsU Create a job. Providing scheduled_start sets status to scheduled.
jobs.updatewrite:jobsU Update mutable fields. Setting status="done" stamps completed_at.
jobs.cancelwrite:jobsU 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.

ToolScopeWhat it does
job_series.listread:job_seriesList recurring series templates. Use jobs.list with a seriesId filter to see individual instances.
job_series.getread:job_seriesFetch one series including template line items and assignments.
job_series.createwrite:job_seriesU 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.updatewrite:job_seriesU Updates apply to all future instances — future jobs are regenerated; completed and cancelled jobs are left intact.
job_series.endwrite:job_seriesEnd 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.

ToolScopeWhat it does
invoices.listread:invoicesList invoices (newest first). Voided invoices are excluded unless a status filter is given.
invoices.getread:invoicesFetch one invoice with fields and computed totals.
invoices.createwrite:invoicesU Create a draft invoice. Totals and tax are computed server-side.
invoices.updatewrite:invoicesU Status transitions are allowed at any time; other fields are editable only while in draft. Line items replace wholesale.
invoices.voidwrite:invoicesVoid 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.

ToolScopeWhat it does
quotes.listread:quotesList quotes (newest first). Archived quotes excluded unless a status filter is given.
quotes.getread:quotesFetch one quote.
quotes.createwrite:quotesU Create a draft quote. Owner-gated.
quotes.updatewrite:quotesU Update shell fields. Illegal status transitions return a conflict error. Owner-gated.
quotes.archivewrite:quotesU 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.

ToolScopeWhat it does
catalog_items.listread:catalog_itemsList catalog items across all kinds: service, product, labor, fee, bundle, discount.
catalog_items.getread:catalog_itemsFetch one item. Bundle components are included.
catalog_items.createwrite:catalog_itemsU Create an item. The kind field selects the variant.
catalog_items.updatewrite:catalog_itemsU Partial patch. Passing components replaces a bundle's component list atomically.
catalog_items.archivewrite:catalog_itemsU Soft-archive a catalog item.

Catalog categories — read:catalog_categories / write:catalog_categories

See Catalog categories API.

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.

ToolScopeWhat it does
catalog_categories.listread:catalog_categoriesList categories as flat rows with parent_id for client-side tree building.
catalog_categories.getread:catalog_categoriesFetch one category by ID.
catalog_categories.createwrite:catalog_categoriesU Create a category. Name must be unique within the parent.
catalog_categories.updatewrite:catalog_categoriesU Partial patch. Cycle re-parenting is a conflict error.
catalog_categories.deletewrite:catalog_categoriesHard 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.

ToolScopeWhat it does
plans.listread:plansList recurring plans (newest first). Filter by status.
plans.getread:plansFetch one plan including line items in sort order.
plans.createwrite:plansU 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.updatewrite:plansU Partial patch. Items replace wholesale. Customer is immutable.
plans.cancelwrite:plansU Cancel a plan. Terminal — no resume, no refund.

Tax rates — read:tax_rates / write:tax_rates

ToolScopeWhat it does
tax_rates.listread:tax_ratesList tax rates including the tenant default. Pass include_archived to see archived rates.
tax_rates.getread:tax_ratesFetch one rate. The rate_decimal field stores the decimal form (for example 0.0825 = 8.25%).
tax_rates.createwrite:tax_ratesCreate a rate. Pass rate_percentage as a number like 8.25. Set is_default: true to make it the account default.
tax_rates.updatewrite:tax_ratesPartial patch. is_default toggles the tenant default.
tax_rates.archivewrite:tax_ratesSoft-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 the cursor value from the previous response to fetch the next page. Check hasMore to know if there are more results, and read nextCursor to get the value to pass next time.
  • Page-based (all other list tools): pass a page integer starting at 1.

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, and agent_sessions scopes exist for other Zoop surfaces and do not yet back any MCP tools.