Install
$ agentstack add mcp-klodr-faxdrop-mcp ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
📠 faxdrop-mcp
> Send real faxes from any MCP-enabled AI assistant. Wraps the FaxDrop HTTP API.
[](https://github.com/klodr/faxdrop-mcp/actions/workflows/ci.yml) [](https://github.com/klodr/faxdrop-mcp/actions/workflows/codeql.yml) [](https://vitest.dev) [](https://codecov.io/gh/klodr/faxdrop-mcp) [](https://scorecard.dev/viewer/?uri=github.com/klodr/faxdrop-mcp) [](https://www.bestpractices.dev/projects/12578) [](https://socket.dev/npm/package/faxdrop-mcp) [](https://coderabbit.ai)
[](https://www.npmjs.com/package/faxdrop-mcp) [](https://www.npmjs.com/package/faxdrop-mcp) [](https://nodejs.org) [](https://modelcontextprotocol.io) [](https://modelcontextprotocol.io) [](https://github.com/klodr/faxdrop-mcp/pulls)
[](https://github.com/sponsors/klodr) [](https://www.patreon.com/klodr) [](https://ko-fi.com/klodr)
A Model Context Protocol (MCP) server that lets AI assistants (Claude, Cursor, Continue, OpenClaw…) send real faxes through the FaxDrop API.
✨ Why this MCP?
Faxing is still required by US healthcare, government forms, and a long tail of legal/financial workflows. FaxDrop is a hosted fax service with a clean HTTP API and a free tier (2 faxes/month). This MCP exposes it to LLMs with the safeguards an agent platform actually needs.
🤔 Why not just call the FaxDrop API directly?
You can. But every agent that does ends up re-implementing the same handful of guards. This MCP gives them to you for free:
- Input validation — absolute-path + extension + 10 MB cap on the upload (all before the file is opened); E.164 regex on the fax number; no SSRF, no path traversal.
- TOCTOU-safe read — file descriptor pinned with
fs.open(), size enforced continuously while reading. - No secret leakage — error objects strip the response body; the audit log keeps only an explicit allowlist of FaxDrop response-shape fields (
recipientNumber,faxId,id) in clear, blocks the credential set (apiKey/authorization/password/ …), and elides every other field with a length marker ([ELIDED:NNN]). Property-tested with fast-check. - Dry-run + audit log —
FAXDROP_MCP_DRY_RUN=trueto test prompts without sending;FAXDROP_MCP_AUDIT_LOG=/abs/pathfor a JSONL trail (mode0o600). - Clean errors — FaxDrop's 402 / 429 / 4xx surfaced as MCP
isErrorwitherror_type,hint,retry_after. - Drop-in for any MCP client — one
npx -y faxdrop-mcpline in Claude Desktop / Code / Cursor / Continue / OpenClaw. - Verifiable releases — Sigstore-signed + SLSA in-toto attestation + npm provenance ([verify](.github/SECURITY.md#verifying-releases)).
A ~12 KB wrapper that turns a one-week security review into a one-line config change.
📦 Installation
npm install -g faxdrop-mcp
Or use directly with npx:
npx faxdrop-mcp
⚙️ Configuration
The server reads FAXDROP_API_KEY from the environment. Get your key at faxdrop.com/account (Developer API → Generate Key). Keys look like fd_live_.
🤖 Claude Desktop / Claude Code
Add to ~/Library/Application Support/Claude/claude_desktop_config.json (or ~/.claude.json for Claude Code):
{
"mcpServers": {
"faxdrop": {
"command": "npx",
"args": ["-y", "faxdrop-mcp"],
"env": {
"FAXDROP_API_KEY": "fd_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
}
}
}
🖱️ Cursor
Add to ~/.cursor/mcp.json:
{
"mcpServers": {
"faxdrop": {
"command": "npx",
"args": ["-y", "faxdrop-mcp"],
"env": {
"FAXDROP_API_KEY": "fd_live_..."
}
}
}
}
🦀 OpenClaw
Add to ~/.openclaw/openclaw.json, then restart the gateway (docker restart openclaw-openclaw-gateway-1 or your equivalent).
🛠️ Tools (3)
📤 faxdrop_send_fax
Send a fax. Uploads a local document from the outbox (default ~/FaxOutbox/) to a fax number in international (E.164) format.
Required:
filePath(string, absolute) — PDF, DOCX, JPEG, or PNG, ≤10 MB. Must live inside the outbox.recipientNumber(string) — E.164, e.g.+12125551234. Subject to the 3-layer phone gate (TYPE → COUNTRY → per-number).senderName(string)senderEmail(string)
Optional cover-page fields (printed only when includeCover is true):
includeCover(boolean) — free accounts always include a branded cover; paid accounts default to falsecoverNote(string, ≤500) — message bodyrecipientName(≤50),subject(≤200),senderCompany(≤100),senderPhone(validated E.164)
Notifications:
sendEmail(boolean) — whether FaxDrop should email the sender a per-fax delivery confirmation. Defaults tofalse(suppress) — the MCP is built for batch/agent workflows where one email per fax floods the inbox, and the same information is already retrievable viafaxdrop_get_fax_status. Set totrueto opt back in. Failed-fax emails, operator alerts, refunds and status pages are unaffected. The send response echoesdeliveryEmail: enabled | suppressed.
Returns: { success, faxId, status, statusUrl, deliveryEmail }
🔗 faxdrop_pair_number
Add a fax number to the paired whitelist (~/.faxdrop-mcp/paired.json). Only effective when FAXDROP_MCP_NUMBER_GATE=pairing (default). The number must still pass the TYPE and COUNTRY checks (no bypass). Always confirm with the user before pairing — paired numbers can be faxed without further per-number approval.
Required:
recipientNumber(string) — E.164
Returns: { paired, country, type }
📊 faxdrop_get_fax_status
Check the delivery status of a previously sent fax. Terminal statuses (delivered / failed / partial) are cached process-wide (LRU 100 entries, whitelist-sliced) — re-polling a finished fax short-circuits with a _cached: true marker to spare your FaxDrop quota.
Recommended polling cadence: every ~5s for the first 2 min, then every ~30s for up to 10 min, stop on terminal status.
Required:
faxId(string)
Returns: { id, status, recipientNumber?, pages?, completedAt?, _cached? }
🛡️ Safeguards
| Knob | Env var | Default | Notes | |---|---|---|---| | Outbox jail | FAXDROP_MCP_WORK_DIR=/abs/path | ~/FaxOutbox/ (auto-created mode 0o700) | Every filePath must live inside this directory after realpath canonicalization. Symlinks to outside the outbox are rejected. | | Number gate | FAXDROP_MCP_NUMBER_GATE=open\|pairing\|closed | pairing | pairing requires HITL approval via faxdrop_pair_number before a new number can be faxed. closed disables runtime pairing (paired.json edited out-of-band). | | Allowed types | FAXDROP_MCP_ALLOWED_TYPES=... | FIXED_LINE,FIXED_LINE_OR_MOBILE,VOIP,TOLL_FREE | libphonenumber NumberType allow-list. | | Allowed countries | FAXDROP_MCP_ALLOWED_COUNTRIES=... | US,CA,PR,GU,VI,AS,MP | ISO-3166-1 alpha-2 allow-list (US/CA + US territories). | | State directory | FAXDROP_MCP_STATE_DIR=/abs/path | ~/.faxdrop-mcp/ (mode 0o700) | Where paired.json lives (mode 0o600, atomic write). | | Dry run | FAXDROP_MCP_DRY_RUN=true | off | Write tools (faxdrop_send_fax, faxdrop_pair_number) return the would-be payload (passed through the same allowlist redaction as the audit log — see row below) and never call FaxDrop or touch paired.json. | | Audit log | FAXDROP_MCP_AUDIT_LOG=/abs/path/audit.log | off | Append-only JSON Lines (file mode 0o600). Allowlist-based redaction: only FaxDrop response-shape fields (recipientNumber, faxId, id) are kept in clear; known credential keys (apiKey / authorization / password / secret / token) are replaced with [REDACTED]; every other field is elided with a [ELIDED:NNN] length marker. This is a fail-closed design: a new API field added upstream is hidden by default, not leaked. |
⚠️ Error catalog
Every failure is returned as isError: true with a structured error_type, message, and (when applicable) hint and retry_after. Programmatic consumers can match on error_type (in structuredContent) to drive retry logic.
| error_type | Layer | Trigger | Suggested action | |---|---|---|---| | phone_parse | input | Recipient number can't be parsed by libphonenumber. | Ask user for an E.164 number. | | phone_type | policy | Phone type (e.g. MOBILE) not in FAXDROP_MCP_ALLOWED_TYPES. | Use a fax line, or extend the env var. | | phone_country | policy | Country not in FAXDROP_MCP_ALLOWED_COUNTRIES. | Confirm with the user; extend the env var if intentional. | | phone_gate | policy | Number not in paired.json and gate is pairing or closed. | In pairing mode: call faxdrop_pair_number first. In closed: edit paired.json out-of-band. | | pair_disabled | policy | faxdrop_pair_number called outside pairing mode. | Set FAXDROP_MCP_NUMBER_GATE=pairing. | | bad_request | filesystem | Path is relative, outside outbox, leaf-symlink, missing, oversized, or has an unsupported extension. | The accompanying hint describes the exact remedy. | | unauthorized | upstream | FaxDrop returned 401. | Check FAXDROP_API_KEY in your MCP client config. | | payment_required | upstream | FaxDrop returned 402 (out of credits). | Top up at the FaxDrop pricing page. | | rate_limited | upstream | FaxDrop returned 429. | Wait retry_after seconds; the hint shows the bucket that was hit. | | invalid_response | upstream | FaxDrop returned a non-JSON body (proxy interception, incident page). | Body is discarded for safety; check FaxDrop status page. | | fax_error | upstream (fallback) | FaxDrop returned an error with no error_type field. | Read the message; treat as transient. |
🚦 Rate limits & quotas
Two independent caps gate every fax send, both enforced by FaxDrop:
- Per-key rate limits (per-minute / per-hour / per-day buckets) —
429 rate_limitedwithretry_afterandX-RateLimit-*headers. - Account credit balance —
402 payment_requiredwhen you run out, with a top-up hint.
The MCP does not add its own limiter; it forwards FaxDrop's response as a clean isError: true with error_type, hint, and retry_after. See FaxDrop's API docs for the current numbers.
🔒 Security
- Always confirm with the user (recipient, file, cover-page) before invoking
faxdrop_send_fax. This is also baked into the tool description. - The MCP reads files from the user's local filesystem — only expose this server to agents you trust.
- Test prompts safely with
FAXDROP_MCP_DRY_RUN=true. - See [SECURITY.md](.github/SECURITY.md) for the vulnerability reporting process.
🗺️ Roadmap
See [ROADMAP.md](docs/ROADMAP.md).
🌐 Ecosystem
Other MCP servers in the klodr family:
- 📧 klodr/gmail-mcp — Gmail
- 📠 klodr/faxdrop-mcp — Send real faxes via FaxDrop (you are here)
- 🏦 klodr/mercury-invoicing-mcp — Mercury banking + invoicing
🤝 Contributing
PRs welcome. See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for the test/build/lint checklist and release process.
📄 License
MIT — see [LICENSE](./LICENSE).
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: klodr
- Source: klodr/faxdrop-mcp
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.