Skip to main content
Glama
MSPbotsAIFramework

Kaseya Quote Manager MCP Server

README.md
# Kaseya Quote Manager MCP Server

An [MCP](https://modelcontextprotocol.io) server for the [Kaseya Quote Manager](https://www.kaseya.com/products/quote-manager/)
API (v1), the quoting and procurement tool MSPs use to quote, sell and order hardware, software
and services. It lets an AI agent read quotes with their sections and lines, sales orders with
lines and payments, purchase orders with lines and costs, suppliers, the product catalog, and
customers with their contacts and addresses. The API is read-only, so every tool is too.

- Transport: MCP Streamable HTTP at `POST /mcp`, health probe at `GET /health`
- Stateless and multi-tenant: credentials arrive on every request as HTTP headers and are
  never stored; every tool is listed from the first `tools/list`
- Also provides three MCP prompts: `quote-pipeline-review`, `quote-detail` (quote_number),
  `purchasing-summary`
- Python 3.12, [uv](https://docs.astral.sh/uv/), FastMCP, httpx
- License: Apache-2.0, Copyright 2026 MSPbots.ai

## Authentication headers

| Header | Required | Type | Default | Allowed values | Description | Example |
|---|---|---|---|---|---|---|
| `X-Kaseya-Quote-Manager-Api-Key` | Yes | string | none | - | Kaseya Quote Manager API key | `your-api-key` |

Where to get it: in Kaseya Quote Manager open **My account (profile icon) > Developer API**.

How the key is sent: every call goes to `https://api.kaseyaquotemanager.com/v1/...` with the key in
the `apiKey` request header (never in the URL, so it stays out of access logs).

A request to `/mcp` without the header gets HTTP 401.

## Tools

All tools return a JSON string. Lists take `page` (from 1) and `pageSize` (max 100) and return
`{count, page, has_more, next_page}`; most also take `modifiedAfter` (ISO date-time) to return only
records changed since then. Every `_get` tool takes **id**.

| Area | Tools |
|---|---|
| Overview | `kqm_navigate` (**domain**: sales, procurement, catalog, crm, org), `kqm_status` |
| Sales | `kqm_quote_list` (quoteNumber), `kqm_quote_get`, `kqm_quote_section_list` (quoteID), `kqm_quote_section_get`, `kqm_quote_line_list` (quoteSectionID), `kqm_quote_line_get`, `kqm_sales_order_list` (orderNumber), `kqm_sales_order_get`, `kqm_sales_order_line_list` (salesOrderID), `kqm_sales_order_line_get`, `kqm_sales_order_payment_list` (salesOrderID), `kqm_sales_order_payment_get` |
| Procurement | `kqm_purchase_order_list` (orderNumber), `kqm_purchase_order_get`, `kqm_purchase_order_line_list` (purchaseOrderID), `kqm_purchase_order_line_get`, `kqm_purchase_order_cost_list` (purchaseOrderID), `kqm_purchase_order_cost_get`, `kqm_supplier_list`, `kqm_supplier_get`, `kqm_product_supplier_list` (productID), `kqm_product_supplier_get` |
| Catalog | `kqm_product_list` (manufacturerPartNumber), `kqm_product_get`, `kqm_product_image_list` (productID), `kqm_category_list`, `kqm_category_get`, `kqm_brand_list`, `kqm_brand_get` |
| CRM | `kqm_customer_list`, `kqm_customer_get`, `kqm_customer_address_list` (customerID), `kqm_customer_address_get`, `kqm_contact_list` (customerID), `kqm_contact_get` |
| Organisation | `kqm_employee_list`, `kqm_employee_get`, `kqm_warehouse_list`, `kqm_warehouse_get` |

Errors come back as a JSON envelope instead of an exception:

```json
{"error": {"code": "unauthorized", "message": "Kaseya Quote Manager returned HTTP 401 for quote", "retryable": false}}
```

`code` is one of `not_configured`, `unauthorized`, `not_found`, `invalid_argument`,
`rate_limited`, `upstream_error`. Requests are retried with backoff on 429, 5xx and network
errors. Kaseya Quote Manager allows 60 calls per minute and 20,000 per day.

## Run locally

```bash
uv sync
uv run python -m kaseya_quote_manager_mcp          # listens on 0.0.0.0:8080
```

or with Docker:

```bash
docker build --platform linux/amd64 -t kaseya-quote-manager-mcp:dev .
docker run --rm -p 8080:8080 kaseya-quote-manager-mcp:dev
```

Configuration (environment variables, all optional): `MCP_HTTP_HOST` (default `0.0.0.0`),
`MCP_HTTP_PORT` (default `8080`), `LOG_LEVEL` (default `INFO`). Credentials are never read
from the environment.

## Testing example

```bash
# list tools
curl -s http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "X-Kaseya-Quote-Manager-Api-Key: your-api-key" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

# quotes changed this month
curl -s http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "X-Kaseya-Quote-Manager-Api-Key: your-api-key" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"kqm_quote_list","arguments":{"modifiedAfter":"2026-10-01T00:00:00Z"}}}'
```

Unit tests (no network access needed):

```bash
uv run pytest
```

## Delivery notes

- **Tool count (41) and prompts (3).** Kept as is because the names are an existing contract; the
  parameters of every tool match the earlier version and the official API exactly.
- **API source.** The official OpenAPI document is published at
  `https://api.kaseyaquotemanager.com/docs/v1/swagger.json` (API Reference v1, 39 read-only
  endpoints); the getting-started guide is in the Kaseya Quote Manager help center.
- **Paging.** The API returns plain arrays with no total; `has_more` is true when a page comes
  back full, and `pageSize` is capped at the API's maximum of 100.
- **All tools are read-only** and annotated as such, matching the API.

## License

Apache License 2.0 - see [LICENSE](LICENSE) and [NOTICE](NOTICE).