Skip to main content
Glama
McDonnies
by McDonnies
README.md
![tests](https://github.com/McDonnies/servicetitan-mcp/actions/workflows/tests.yml/badge.svg)

# servicetitan-mcp

A read-only [MCP](https://modelcontextprotocol.io) server over the ServiceTitan API: six task-shaped tools (find a customer, look at their jobs, their money, resolve an external id) instead of a thin wrapper over ServiceTitan's own endpoints. `ReadOnlyClient` defines only `get` and `get_page` — no write path exists.
See [`docs/DESIGN.md`](docs/DESIGN.md) for the reasoning behind these choices.

## The credentials rule

The MCP client config carries exactly one setting: `ST_ENV_FILE`, a path — never a secret directly. Everything else (client id, secret, app key, tenant id) lives in the file it points at, `chmod 600`, outside the repo and outside any MCP config: MCP configs get pasted into chats, and some (`.mcp.json`) are designed to be committed.
`permission_warning` checks the file's mode at startup and warns — doesn't refuse to start — if it's looser than `600`. Full reasoning in [`docs/DESIGN.md`](docs/DESIGN.md#credentials).

```json
{
  "mcpServers": {
    "servicetitan": {
      "command": "/absolute/path/to/servicetitan-mcp/.venv/bin/servicetitan-mcp",
      "env": { "ST_ENV_FILE": "/home/you/.config/servicetitan-mcp/env" }
    }
  }
}
```

`command` must be an absolute path into this project's `.venv` — an MCP client launches from an arbitrary directory, so a bare `python` may resolve to an interpreter without `mcp`/`httpx`/`st_mcp` installed.

## The six tools

| Tool | Answers | Reach for it when |
|---|---|---|
| `find_customer` | Who is this, by name/phone/external id? | You have a human identifier, not a ServiceTitan id. One match returns the full card; several return a short list to disambiguate. |
| `customer_overview` | Full card for one customer: contacts, locations, 5 recent jobs, external ids. | You already have the numeric customer id — from `find_customer` or a prior call. |
| `find_jobs` | Which jobs match these filters, and when's the next visit? | You want a list of jobs by customer, date range, or status. Not for finding a customer — that's `find_customer`. |
| `job_detail` | Everything about one job: appointments, technicians. | You have a job id and need its full appointment history. |
| `customer_money` | Balance, estimates, invoices, payments — for one customer, one call. | Any "what do they owe" or billing-history question. |
| `find_by_external_id` | Which ServiceTitan customer has this id in another system? | You're crossing over from a CRM synced into ServiceTitan via `externalData` (requires `ST_APP_GUID`). |

Job types, business units, and payment types are also exposed as MCP **resources** (cached client-side) rather than tools, and back the id→name substitution above.

## Token savings, measured against a live tenant

`chars / 4`, on the same four ServiceTitan reads:

| | tokens |
|---|---|
| `customer_overview`, raw JSON pretty-printed | 1516 |
| `customer_overview`, raw JSON minified | 1001 |
| `customer_overview`, this server's output | **69** |
| `customer_money` | 43 |
| `find_jobs`, one customer | 28 |
| `find_jobs`, 20 jobs with next-visit dates | 318 |

22x against pretty-printed JSON, 15x against minified, per call — and `customer_overview` is one model round trip for four ServiceTitan reads, where an endpoint-shaped tool surface would spend four.

## ServiceTitan findings

- `totalCount` is `null` unless the request sends `includeTotal=true`.
- `jpm/appointments` has no batch filter — `jobIds`, `jobId=<csv>`, repeated `jobId` are all ignored or wrong; only `jobId=<single>` works.
- `sales/estimates` ignores `customerId`; reachable only via `jobId` or `locationId`.
- A 404 on an appointment's assigned-technicians means "nobody assigned yet", not "not found" — the API's own "could not" that actually means "none".

## Setup

Requires Python 3.11+.

```bash
python -m venv .venv
.venv/bin/pip install -e ".[dev]"
```

Create the env file `ST_ENV_FILE` will point at (e.g. `~/.config/servicetitan-mcp/env`), `chmod 600` it, and fill in:

```
ST_CLIENT_ID=...
ST_CLIENT_SECRET=...
ST_APP_KEY=...
ST_TENANT_ID=...
# Optional:
ST_AUTH_HOST=auth.servicetitan.io
ST_API_HOST=api.servicetitan.io
ST_APP_GUID=...            # required only for find_by_external_id
ST_EXTERNAL_ID_KEY=...     # the externalData key find_customer treats as an id
```

`ST_AUTH_HOST`/`ST_API_HOST` default to production; ServiceTitan's sandbox uses different hosts (`*-integration.*`) and both must match the credentials' environment, or the token request 400s with `invalid_client`.

### Registering it with a client

Both routes need an absolute path to the `servicetitan-mcp` console script in this project's `.venv`, and `ST_ENV_FILE`. Neither carries a secret.

**Claude Code**:

```bash
claude mcp add servicetitan -s user \
  -e ST_ENV_FILE=/home/you/.config/servicetitan-mcp/env \
  -- /home/you/servicetitan-mcp/.venv/bin/servicetitan-mcp
```

`-s user` registers it for every project on the machine — prefer it over `-s project`, which writes a committable `.mcp.json`. Verify with `claude mcp list` (`✔ Connected`).

**Claude Desktop**: copy `claude_desktop_config_example.json`, fix the two paths, and merge it into the client's own config.

Run the tests with `.venv/bin/pytest`. Nothing in the suite makes a live ServiceTitan call — every test uses the fake clients in `tests/conftest.py`.

TDQS

A3.9/5.0

Scored across 6 tools

Disambiguation3/5

find_customer, customer_overview, and find_by_external_id all locate customers, with find_customer already supporting external IDs, causing potential confusion. However, descriptions clarify that customer_overview requires a ServiceTitan ID and find_by_external_id needs a specific externalData key, so agents can usually pick correctly.

Naming Consistency3/5

Tool names mix patterns: find_customer and find_jobs use verb_noun, customer_overview and job_detail use noun_noun, and find_by_external_id uses verb_preposition_noun. All are snake_case and readable, but the inconsistent structure makes it harder to predict tool names.

Tool Count4/5

Six tools is a reasonable count for a customer/job lookup server, not overwhelming. However, the overlap between find_customer and find_by_external_id means the count is slightly padded, preventing a perfect score.

Completeness3/5

The server covers customer retrieval, job lookups, and billing summaries well for read-only scenarios. Missing write operations like creating/updating customers or jobs, and there is no direct way to fetch all appointments independently, which are notable gaps for a full lifecycle.

Maintenance

ActivityMaintained
ResponsivenessNo issues