Housecall Pro MCP
[CI]
[npm]
[license]
A Model Context Protocol server that connects
Claude to the customer side of Housecall Pro —
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. 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:
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 my plumber 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
Configure it with the link your contractor sent you:
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:
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.
Tools
Response shape (view)
housecallpro_get_estimate and housecallpro_get_invoice take
view: 'compact' | 'raw', defaulting to compact — the fleet vocabulary
from @chrischall/mcp-utils.
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): 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 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
License
MIT