TimeZest MCP Server
README.md
# TimeZest MCP Server
An [MCP](https://modelcontextprotocol.io) server for the [TimeZest](https://www.timezest.com)
API, the appointment-scheduling service MSPs use so their customers can book time with
technicians from a PSA ticket. It lets an AI agent read agents, teams, appointment types,
bookable resources and scheduling requests and, on explicit request, create a scheduling
request (optionally linked to a PSA ticket).
- Transport: MCP Streamable HTTP at `POST /mcp`, health probe at `GET /health`
- Stateless and multi-tenant: the API key arrives on every request as an HTTP header and is
never stored; every tool is listed from the first `tools/list`
- 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-Timezest-Api-Token` | Yes | string | none | - | TimeZest API key (a leading `Bearer ` is accepted and ignored) | `your-api-token` |
Where to get it:
- A TimeZest administrator creates an API key on the **API Keys** page of the TimeZest web app
(`app.timezest.com` -> Settings -> API Keys).
- A **read-only** key is enough for every tool except `timezest_scheduling_create_request`,
which needs a **read/write** key.
- TimeZest allows 180 requests per minute per account (all keys combined).
How the key is sent to TimeZest: `Authorization: Bearer <key>` on every call to
`https://api.timezest.com/v1/...`.
A request to `/mcp` without the header gets HTTP 401 naming it.
## Tools
All tools return a JSON string. TimeZest returns lists newest first in fixed pages of 20; list
tools return `{items, count, has_more, next_cursor}` and take `startingAfter` (the previous
`next_cursor`), `pageSize` (return at most this many of the page) and `filter`, a
[TimeZest Query Language](https://developer.timezest.com/tql/) expression such as
`scheduling_request.end_user_email LIKE @example.com`. Timestamps are returned as ISO 8601 UTC.
| Tool | Purpose | Parameters (required in **bold**) |
|---|---|---|
| `timezest_status` | Check the connection (the API key works) and list the tool groups | - |
| `timezest_agents_list` | List agents (technicians): name, email, role, whether schedulable | pageSize, filter, startingAfter |
| `timezest_agents_get` | One agent | **agentId** |
| `timezest_teams_list` | List teams (pools of agents) and their type | pageSize, filter, startingAfter |
| `timezest_teams_get` | One team | **teamId** |
| `timezest_appointment_types_list` | List appointment types with default duration | pageSize, filter, startingAfter |
| `timezest_appointment_types_get` | One appointment type | **appointmentTypeId** |
| `timezest_resources_list` | List bookable resources (agents and teams); their ids go into `resourceIds` | pageSize, type (`agent`, `team`), filter, startingAfter |
| `timezest_scheduling_list` | List scheduling requests: end user, type, status, chosen time, URL | pageSize, status (`new`, `sent`, `scheduled`, `cancelled`, `closed`; also `pending`, `booked`, `completed`), filter, startingAfter |
| `timezest_scheduling_get` | One scheduling request with resources, booked agents and booking URL | **requestId** |
| `timezest_scheduling_create_request` | Create a scheduling request; TimeZest emails the customer (**write**) | **appointmentTypeId**, **triggerMode** (`pod`, `generate_url`), **endUser** {**name**, email, company}, timeRange {earliestDate, earliestTime, latestDate, latestTime}, resourceIds, associatedEntities [{**type**, id, number}], durationMins, guests |
| `timezest_scheduling_cancel` | Explain how to cancel a request (the API cannot cancel); returns its status and URL | **requestId**, reason |
`associatedEntities[].type` is one of `autotask/company`, `autotask/contact`, `autotask/ticket`,
`connectwise_psa/company`, `connectwise_psa/contact`, `connectwise_psa/project_ticket`,
`connectwise_psa/service_ticket`, `halo_psa/user`, `halo_psa/ticket`, `service_now/incident`,
`service_now/problem`, `service_now/change_request`, `service_now/sn_customerservice_case`,
`service_now/contact`. When a PSA contact or ticket is linked, TimeZest can take the end
user's name and email from it.
Errors come back as a JSON envelope instead of an exception:
```json
{"error": {"code": "unauthorized", "message": "TimeZest returned HTTP 401 for agents: Unauthorized. No valid API key provided", "retryable": false}}
```
`code` is one of `not_configured`, `unauthorized`, `not_found`, `invalid_argument`,
`rate_limited`, `upstream_error`. Reads are retried with backoff on 429, 5xx and network
errors; the create call is retried only on 429, so a request is never created twice.
## Run locally
```bash
uv sync
uv run python -m timezest_mcp # listens on 0.0.0.0:8080
```
or with Docker:
```bash
docker build --platform linux/amd64 -t timezest-mcp:dev .
docker run --rm -p 8080:8080 timezest-mcp:dev
```
Configuration (environment variables, all optional): `MCP_HTTP_HOST` (default `0.0.0.0`),
`MCP_HTTP_PORT` (default `8080`), `LOG_LEVEL` (default `INFO`). The key is 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-Timezest-Api-Token: your-api-token" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
# bookings waiting for the customer to pick a time
curl -s http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "X-Timezest-Api-Token: your-api-token" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"timezest_scheduling_list","arguments":{"status":"sent"}}}'
```
Unit tests (no network access needed):
```bash
uv run pytest
```
## Delivery notes
- **Tool count (12).** Kept as is because the tool names are an existing contract.
- **Cancelling.** The TimeZest API has no cancel operation, so `timezest_scheduling_cancel`
changes nothing: it returns the request's status and `scheduling_url` (the customer's
reschedule / cancel page) and tells the agent to have it cancelled in TimeZest or the PSA.
- **Create request.** `appointmentTypeId` is required (TimeZest requires it; older versions
silently picked the first type). `resource_ids` is always sent as an array. Associated
entities use TimeZest's own type names (for example `connectwise_psa/service_ticket`), times
are sent as `HH:MM:SS`, and fields TimeZest does not accept (`notes`, end-user `phone`, time
zone) were removed instead of being silently dropped.
- **Lookups by id.** TimeZest has no get-by-id for agents, teams or appointment types, so the
`_get` tools walk the list pages (at most 50 pages) to find the id.
- **No interactive prompts.** Older versions asked the user to pick an appointment type or
confirm a cancel mid-call, which a stateless gateway cannot relay.
## License
Apache License 2.0 - see [LICENSE](LICENSE) and [NOTICE](NOTICE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues