Releases: pghdma/callrail-mcp
Release list
v1.1.3 — Mutation-testing audit: vacuous tests fixed, py.typed shipped
[1.1.3] - 2026-07-03
Fixed — deep-audit round 4: mutation testing + shipped-artifact audit
Round 4 turned the audit on the TEST SUITE and the PACKAGING — layers
no code-reading or fuzzing round can reach. A 14-mutant battery
(deliberate bugs injected into critical lines, one at a time) exposed
two vacuous regression tests guarding changelog-advertised guarantees:
test_v042_post_does_NOT_retry_on_5xxnever tested its claim.
The fixture builds the client withmax_retries=0— NOTHING retries
in that configuration, so the test passed even with POST added to
the idempotent-retry set. The CRITICAL double-write guard from
v0.4.2 (duplicate $3/mo trackers) had zero effective coverage. Now
runs with retries enabled.test_v050_compare_periods_no_overlapasserted on its own local
re-implementation of the window arithmetic — it never called
compare_periods, so the v0.5.0 double-count fix could regress
without any test failing. Rewritten end-to-end against the window
boundaries the tool actually returns.- 36500-day cap had no boundary coverage — a mutant raising the
cap 1000× survived (the existing test used 10**18, which any large
cap rejects). Added days=36500/36501 boundary test. - 1 equivalent mutant identified and documented (paginate total_pages
min-clamp is shadowed by the while-loop bound — differs only in
warning emission). Final score: 13/13 non-equivalent mutants killed.
Fixed — packaging
py.typedwas missing from the wheel (PEP 561). The project
ships mypy-strict annotations and documents library usage, but
without the marker downstream type checkers ignore ALL annotations.
Added marker + package-data config; verified present in the built
wheel.
Documentation
- Docstring Args drift fixed on
update_user(5 params undocumented),
get_text_message,get_webhook(account_id undocumented) — found
by a mechanical docstring-vs-signature sweep across all 59 tools.
Verified
- Shipped PyPI artifact (1.1.2) smoke-tested in a clean venv on
Python 3.14: installs, imports, registers all 59 tools. - Tests 395 → 396.
v1.1.2 — Response-shape fuzz + property-testing hardening
[1.1.2] - 2026-07-03
Fixed — deep-audit round 3: response-shape fuzz + property-based testing
Round 2 fuzzed tool INPUTS; round 3 fuzzed what CallRail sends BACK
(2,891 calls with structurally-valid-but-garbage-typed responses) and
brought hypothesis property testing to invariants no prior round had
mechanically verified.
Response-shape hardening (182 fuzz failures, 8 roots)
paginate()now yields only dict items. A malformed collection
containing strings/ints/nulls crashed all six paginate-consuming
tools (call_summary,usage_summary,compare_periods,
spam_detector,search_calls_by_number,bulk_update_calls) with
raw AttributeError onitem.get(). Non-dict items are skipped with
a warning.call_eligibility_check— wrong-typedutm_source/source/
source_namecrashed.lower(); now safely coerced.search_calls_by_number— non-stringcustomer_phone_numberin
a call record crashed_digits_only; now skipped.
Hypothesis findings (property-based, permanent in
tests/test_properties.py)
_date_windowOverflowError — reachable in production. A
valid-format ancient end_date (e.g. "0001-01-01") minus a large
dayslookback lands before year 1, whichdatecannot represent —
list_calls(days=36500, end_date="0001-01-01")crashed with a raw
OverflowError despite the 36500-day cap. Now clamped to date.min.- Money invariant now machine-verified: per-company cost shares in
usage_summarysum toestimated_cycle_totalexactly (cent-level)
across 120 randomized fleets per run. - Also pinned:
_safe_pathcan never emit a dot-segment or unencoded
reserved char for ANY input;_parse_retry_afteralways lands in
[0, 60] and finite;_is_toll_freeand_date_windoware total
functions over garbage.
Added — tests
tests/test_properties.py— 6 hypothesis property suites.- 4 targeted regressions (ancient end_date, paginate non-dict skip,
wrong-typed eligibility fields, non-string phone field). - Tests 385 → 395.
v1.1.1 — All-tool fuzz hardening (1,051 crash paths closed)
[1.1.1] - 2026-07-03
Fixed — deep-audit round 2: all-tool adversarial fuzz
A mechanical fuzz pass (every tool × every parameter × 17 garbage
values ≈ 5,000 calls) found 1,051 violations of the project's core
error contract ("tools never raise — always return a JSON envelope").
Root cause: the shared validators assumed string inputs; any non-string
type (int, list, bytes, bool, float) sent by a loose-JSON MCP client
for a string field crashed with raw TypeError/AttributeError across
essentially all 59 tools. Prior releases fuzzed only tracker tools, so
the class went unseen.
Shared-validator hardening (fixes ~all 1,051 at the root)
_require_non_empty— rejects non-string types with a clear
"must be a string (got int)" envelope instead of passing them
through to crash downstream._validate_id_shape,_validate_date,_validate_phone,
_validate_email,_validate_length,_validate_area_code,
_validate_pool_size— explicit type guards._clamp_per_page— coerces numeric strings ("250"), returns 1 on
garbage (previously raw TypeError from the<comparison).- New
_clamp_page— replaces 13 inlinemax(1, page)call sites
that raised TypeError on string/None pages. _clean_tag_list— non-list input returns [] with a warning
(mirrors_tag_names_from; previously iterated ints → TypeError).update_call/update_form_submission/create_form_submission
—tagstype-checked as list beforelen().call_eligibility_check— threshold coerced before the< 0
comparison (None/"60" previously raised TypeError).
Behavior kept honest with docstrings
delete_tag(tag_id=812)(int form) now works — the docstring has
promised "accepts string or numeric forms" since v0.6.1 but int
input crashed with TypeError. get_tag/update_tag same.search_calls_by_number(phone_number=4125551234)— a phone number
arriving as a JSON number is now coerced instead of crashing.
Wire safety
- NaN/Infinity rejected in
valueparams (update_form_submission,
create_form_submission,update_sms_thread).json.dumps
serializes non-finite floats as bareNaN/Infinitytokens —
invalid JSON on the wire to CallRail.
Response-shape robustness (client.py)
paginate()— a malformed response with a STRING where the items
array belongs wouldyield fromsingle characters; consumers then
crashed on'str'.get(). Non-list items now stop pagination with
a warning.resolve_account_id()—accountsas a dict raised raw
KeyError: 0; now a catchable CallRailError.
Added — tests
tests/test_fuzz_all_tools.py— the full adversarial matrix
(59 tools × every param × 17 garbage values, ~5k calls) is now a
permanent regression test pinning the never-raise invariant.- 6 targeted regressions (paginate string-items, dict accounts,
NaN value ×3 tools, int tag_id, JSON-number phone, clamp helpers). - Tests 320 → 385.
v1.1.0 — Leads, SMS threads, server-side analytics
[1.1.0] - 2026-07-03
Added — 9 new tools (API caught up with us, so we caught up with it)
CallRail shipped substantial API surface between April and July 2026.
All endpoint shapes below were live-verified against a production
account on 2026-07-03 (read-only probes) before implementation.
list_leads/get_lead_timeline— CallRail's deduplicated
person records + full cross-channel history (calls, forms, texts)
per lead with first/last-touch attribution. Replaces the manual
"search calls by number + search forms by email" reconstruction.list_sms_threads/get_sms_thread/update_sms_thread
— SMS-thread lead management.update_sms_threadcloses the write
gap where texting leads couldn't be tagged / noted / qualified via
API (notes, value, tags withappend_tags, lead_qualification).call_stats— server-side aggregation via/calls/summary.json
(group_by source/keywords/campaign/referrer/landing_page/company).
One request instead of paginating every call.call_timeseries— per-day call-volume trend via
/calls/timeseries.json.form_stats— server-side form totals via/forms/summary.json.get_call_page_views— the visitor's page-view journey behind a
call; pairs withcall_eligibility_checkfor conversion debugging.
Changed — validators updated to CallRail's newly-published enums
CallRail has now documented enums we originally discovered empirically
— and our fail-fast validation had become over-restrictive:
VALID_TAG_COLORS: 10 → 24 values. The docs now publish the full
24-color table; our 10-value tuple was rejecting 14 documented-valid
colors. Live-verified 2026-07-03:create_tag(color="cyan1")
succeeds (probe tag created + deleted).VALID_SOURCE_TYPES: 7 → 12 values. Union of the 10 documented
values (addslanding_url,landing_params,web_referrer,
search,mobile_ad_extension) and the 2 production-proven values
the docs still omit (facebook_all,bing_all).
Changed — transcript gating (CallRail breaking change 2026-05-21)
get_call_transcript404s now carry a disambiguation hint.
CallRail's 2026-05-21 API change gates transcript data behind
Premium Conversation Intelligence — without it the endpoint 404s
even when a transcript exists in the UI. The error envelope now
explains both possible causes instead of returning a bare 404.
Documentation
- README: new "How this compares to CallRail's official MCP server"
section (CallRail shipped a hosted OAuth MCP server ~30 tools;
this project stays pip-installable, local, 59 tools, with agency
cost-attribution tooling the official one lacks). - CLAUDE.md: API-coverage notes refreshed for July 2026 (outbound
caller IDs now documented-but-403, MMS send shipped May 5 but still
needs A2P registration, message flows + integration filters
documented-but-403).
Added — tests
- 14 new tests (306 → 320): all 9 new tools (happy paths + validation
rejections + PUT body shape), enum expansion guards, transcript-404
hint. Tag-color guard test updated to pin the 24-value set.
v1.0.3
v1.0.2
v1.0.1
v1.0.0 — First stable release
First stable release of callrail-mcp 🎉
pipx install callrail-mcp
# or
pip install callrail-mcpWhat's in the box
49 tools spanning ~85% of the CallRail REST API v3:
- Read tools (calls, companies, trackers, users, tags, form submissions, text messages, recordings, transcripts)
- Write tools (update_call, tag CRUD, form submission updates)
- Tracker provisioning (full CRUD with billing-confirmation safeguard)
- Account management (Companies CRUD, Users CRUD)
- Notifications + Integrations (CRUD on alert rules, discovery for GMB / Google Ads / etc.)
- Outbound calling (with
confirm_dialing=Truesafety gate) - Form-submission backfill for offline leads
- Agency aggregation tools:
usage_summary,compare_periods,bulk_update_calls,spam_detector,call_eligibility_check
Out of scope (deliberately)
See README's "Out of scope" section for the documented gaps:
- SMS send + webhook integration CRUD: blocked by CallRail account permissions (return 403 on standard accounts)
- Outbound Caller IDs, Numbers, Call Flows, Custom Fields, DNC list: not exposed by CallRail's REST API (UI-only on standard plans)
PRs welcome from anyone whose CallRail plan exposes those endpoints.
Quality gates
- 297 tests, all green
- 84% line coverage
- All 5 check tools clean:
pytest -W error,mypy --strict,ruff,bandit,pyright - ~115 bugs caught + fixed across 18 development releases (audit-and-test discipline)
See CHANGELOG.md for the full development history.
🤖 Built with the help of Claude Code