Housecall Pro MCP
# Housecall Pro MCP
[](https://github.com/chrischall/housecallpro-mcp/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/@chrischall/housecallpro-mcp)
[](LICENSE)
A [Model Context Protocol](https://modelcontextprotocol.io) server that connects
Claude to the **customer side** of [Housecall Pro](https://housecallpro.com) —
the estimate or invoice link a contractor (HVAC, plumbing, electrical, cleaning)
emails or texts you.
> [!WARNING]
> **AI-developed project.** This codebase was built and is actively maintained
> by [Claude Code](https://www.anthropic.com/claude). No human has audited the
> implementation. Review all code and tool permissions before use.
## This is the customer side, not the business side
Housecall Pro has two surfaces, and they share nothing:
| | Public API | Customer portal (**this repo**) |
| --- | --- | --- |
| Host | `api.housecallpro.com` | `app.housecallpro.com` |
| Serves | the business running on Housecall Pro | that business's customers |
| Auth | an API key from the pro's account | the link your contractor sent you |
| Docs | [docs.housecallpro.com](https://docs.housecallpro.com) | [`docs/HOUSECALLPRO-API.md`](docs/HOUSECALLPRO-API.md) |
If you *run* a business on Housecall Pro, you want the public API instead. This
server is for being someone's customer.
## What you can do
- *"What did Queen City quote me for the tankless flush?"*
- *"What's on that estimate, line by line?"*
- *"How much of that $346 is tax?"*
- *"Am I still on the hook to respond to this?"*
- *"Decline option 2."*
## Install
```sh
npx -y @chrischall/housecallpro-mcp
```
Configure it with the link your contractor sent you:
```sh
HOUSECALLPRO_LINK='https://pro.housecallpro.com/mobile_estimate/XXXXXXXXXX'
```
Estimate and invoice links both work, short or long form —
`pro.housecallpro.com/mobile_estimate/…` and `/mobile_invoice/…`, or
`client.housecallpro.com/estimates/…` and `/invoices/…`. For several documents:
```sh
HOUSECALLPRO_LINKS='[{"label":"tankless","url":"…"},{"label":"hvac","url":"…"}]'
```
Then every tool takes an optional `link` selector; with one configured you never
need it.
> [!IMPORTANT]
> **Your link is a bearer credential.** Anyone holding it can read the document
> and, for an estimate, decline it. It is read from the environment, never logged, and never
> returned in a tool result — `housecallpro_list_links` reports labels only.
### Confirmations
Declining asks you first. A client that can show a confirmation prompt (Claude
Code) shows one. On a client that cannot (claude.ai, Claude Desktop) the first
call declines nothing and returns a preview plus a `confirmToken`; only a repeat
call with that token acts, and only if the estimate still matches what was
previewed.
| variable | default | |
|---|---|---|
| `MCP_CONFIRM_MODE` | `ask-user` | What a write does on a client that cannot show a confirmation prompt (claude.ai, Claude Desktop). `ask-user`: two steps — the first call does nothing and returns a preview plus a token, and the model must get your approval in chat before calling again with it. `auto`: the same two steps, but the model may use the token after reviewing the preview itself. `refuse`: writes are refused on such clients. A client that can show prompts (Claude Code) always gets the real prompt. An unrecognised value is treated as `refuse`. |
| `MCP_CONFIRM_TTL_SECONDS` | `600` | How long a token stays valid. |
| `MCP_CONFIRM_SECRET` | random per process | Signing key; set it only if tokens must survive a server restart. |
## Tools
| Tool | |
| --- | --- |
| `housecallpro_get_estimate` | Line items, totals, tax, company, approval state |
| `housecallpro_get_invoice` | Amount, subtotal, tax, balance due, payability |
| `housecallpro_get_company` | The contractor: phone, email, website, arrival window |
| `housecallpro_list_links` | Configured links, labels only |
| `housecallpro_decline_estimate` | Decline options — asks you to confirm first |
| `housecallpro_approve_estimate` | Always refuses; explains why |
| `housecallpro_healthcheck` | Reachability + whether a link still resolves |
### Response shape (`view`)
`housecallpro_get_estimate` and `housecallpro_get_invoice` take
`view: 'compact' | 'raw'`, **defaulting to `compact`** — the fleet vocabulary
from [`@chrischall/mcp-utils`](https://github.com/chrischall/mcp-utils).
| rung | what you get |
| --- | --- |
| `compact` *(default)* | the summary: line items, totals, tax, company, approval state — with money as both `*_cents` and `*_usd` |
| `raw` | the upstream document verbatim (~4.8 KB for an estimate), `{object, data}` wrappers and display flags included |
There is deliberately **no `full`** rung. `full` means "every field this server
understands, nothing dropped", and the fields this server understands are
exactly the ones the summary names — so it would be `compact` under a second
name, and everything past it is the upstream document, which is `raw`. A schema
should never advertise a value that silently aliases another.
If the upstream shape drifts far enough that the projection loses its footing,
the whole document is returned (with a warning on stderr) rather than an empty
summary: an empty summary is indistinguishable from an estimate with nothing on
it.
### Estimates and invoices are different documents
They use different token shapes — 129 characters for an estimate, 32 for an
invoice — and different endpoints. The client checks the shape and refuses a
token pointed at the wrong tool before spending a request, rather than passing
along an unexplained 404.
An invoice carries **no line items** and **no tax field**: a paid invoice renders
as a summary in the portal and the API returns exactly that, so `tax_usd` is
derived as `total - subtotal`. `is_paid` comes from the balance, not the status
string.
### Money is returned twice
The upstream API returns **integer cents** — the estimate the portal renders as
`$346.39` arrives as `total_amount: 34639`. Reporting that raw overstates every
figure 100×, so each money field is emitted as both `*_cents` (verbatim) and
`*_usd` (derived). `tax.rate` is a fraction (`0.0825` = 8.25%) and is never
scaled.
That pairing is what the projection is *for*, so it exists on `compact` only.
`view: 'raw'` is the upstream document, and its money is integer cents with no
dollar sibling — `total_amount: 34639` is $346.39. The `view` parameter's own
description says so at the call site.
### Why you can't approve an estimate
`housecallpro_approve_estimate` always refuses, and that is deliberate.
Approval posts a `response_token` — a **reCAPTCHA v3 token** minted in-page for
the action `estimates_customer_approvals`. No server-side client can produce
one, and neither can a browser-bridge transport: the bridge issues `fetch`
calls, it does not execute page JS. Declining carries no such token, which is
why decline works and approve does not.
Rather than post a request that would be rejected — or worse, might *not* be,
binding you to a quoted price — the tool refuses and tells you to approve in a
browser.
Declining asks you to confirm first (see [Confirmations](#confirmations)): it
reads the estimate, refuses ids that are not its options or are already decided,
and shows exactly what would be sent before anything is posted. After a real decline it
**re-reads the estimate** and reports the option's actual status, because a 2xx
is not proof a write landed.
## Without the MCP
[`skills/housecallpro`](skills/housecallpro/SKILL.md) does the same reads from a
shell with plain `curl` and `jq`, for scripts or machines where the server isn't
installed. No browser bridge is involved there either.
## No browser bridge
Unlike much of this fleet, `app.housecallpro.com` is not bot-walled — a bare
`curl` gets a `200`. So this server talks to it directly over HTTPS, has no
`@fetchproxy/server` dependency, needs no extension or signed-in tab, and hosts
cleanly as a remote connector — **with no secret to configure**, since the link
travels as a tool argument rather than an environment variable.
## What isn't here
- **Payments and cards.** Deliberately out of scope.
- **The account-level portal.** Housecall Pro has an OTP/magic-link customer
portal that spans every document from one contractor, which is a strictly
better surface than per-document links. Standing it up needs a human to
receive a one-time code, so it is the obvious next increment rather than part
of this first cut.
## Development
```sh
npm install
npm run build
npm test
```
## License
MIT
TDQS
Scored across 7 tools
Each tool targets a distinct resource or action: company lookup, estimate/invoice retrieval, estimate decline/approve, health check, and link listing. The only minor overlap is get_estimate including company info, but that's complementary rather than confusing.
All tools follow the consistent pattern housecallpro_ + verb_noun (e.g., get_estimate, decline_estimate, list_links). Verbs are lowercase and snake_case throughout, making the naming predictable and scannable.
With 7 tools, the server is well-scoped for its purpose—reading and acting on estimates/invoices plus administrative utilities. Each tool earns its place, and the count sits comfortably in the ideal 3-15 range.
The core lifecycle is covered: retrieve documents, decline an estimate, and access company details. The main gap is the lack of a tool to list all estimates/invoices (only get by ID), and payment is not addressed, but these are minor given the server's customer-facing scope.