Skip to main content
Glama
klodr

faxdrop-mcp

by klodr
README.md
# πŸ“  faxdrop-mcp

> Send real faxes from any MCP-enabled AI assistant. Wraps the [FaxDrop](https://faxdrop.com) HTTP API.

[![CI](https://github.com/klodr/faxdrop-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/klodr/faxdrop-mcp/actions/workflows/ci.yml)
[![CodeQL](https://github.com/klodr/faxdrop-mcp/actions/workflows/codeql.yml/badge.svg)](https://github.com/klodr/faxdrop-mcp/actions/workflows/codeql.yml)
[![Tested with Vitest](https://img.shields.io/badge/tested%20with-vitest-yellow?logo=vitest&labelColor=black)](https://vitest.dev)
[![codecov](https://codecov.io/gh/klodr/faxdrop-mcp/branch/main/graph/badge.svg)](https://codecov.io/gh/klodr/faxdrop-mcp)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/klodr/faxdrop-mcp/badge)](https://scorecard.dev/viewer/?uri=github.com/klodr/faxdrop-mcp)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/12578/badge)](https://www.bestpractices.dev/projects/12578)
[![Socket Security](https://socket.dev/api/badge/npm/package/faxdrop-mcp)](https://socket.dev/npm/package/faxdrop-mcp)
[![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/klodr/faxdrop-mcp?labelColor=171717&color=FF570A&link=https%3A%2F%2Fcoderabbit.ai&label=CodeRabbit+Reviews)](https://coderabbit.ai)

[![npm version](https://img.shields.io/npm/v/faxdrop-mcp.svg)](https://www.npmjs.com/package/faxdrop-mcp)
[![npm downloads](https://img.shields.io/npm/dm/faxdrop-mcp.svg)](https://www.npmjs.com/package/faxdrop-mcp)
[![Node.js Version](https://img.shields.io/node/v/faxdrop-mcp.svg)](https://nodejs.org)
[![MCP](https://img.shields.io/badge/MCP-1.29-blue)](https://modelcontextprotocol.io)
[![MCP Server](https://badge.mcpx.dev?type=server 'MCP Server')](https://modelcontextprotocol.io)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/klodr/faxdrop-mcp/pulls)

[![Sponsor on GitHub](https://img.shields.io/github/sponsors/klodr?logo=github-sponsors&label=GitHub%20Sponsors&color=EA4AAA)](https://github.com/sponsors/klodr)
[![Patreon](https://img.shields.io/badge/Patreon-F96854?logo=patreon&logoColor=white)](https://www.patreon.com/klodr)
[![Ko-fi](https://img.shields.io/badge/Ko--fi-FF5E5B?logo=kofi&logoColor=white)](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](https://www.faxdrop.com/for-developers).

## ✨ 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=true` to test prompts without sending; `FAXDROP_MCP_AUDIT_LOG=/abs/path` for a JSONL trail (mode `0o600`).
- **Clean errors** β€” FaxDrop's 402 / 429 / 4xx surfaced as MCP `isError` with `error_type`, `hint`, `retry_after`.
- **Drop-in for any MCP client** β€” one `npx -y faxdrop-mcp` line 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

```bash
npm install -g faxdrop-mcp
```

Or use directly with `npx`:

```bash
npx faxdrop-mcp
```

## βš™οΈ Configuration

The server reads `FAXDROP_API_KEY` from the environment. Get your key at [faxdrop.com/account](https://faxdrop.com/account) (Developer API β†’ Generate Key). Keys look like `fd_live_<32 hex>`.

### πŸ€– Claude Desktop / Claude Code

Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (or `~/.claude.json` for Claude Code):

```json
{
  "mcpServers": {
    "faxdrop": {
      "command": "npx",
      "args": ["-y", "faxdrop-mcp"],
      "env": {
        "FAXDROP_API_KEY": "fd_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
      }
    }
  }
}
```

### πŸ–±οΈ Cursor

Add to `~/.cursor/mcp.json`:

```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 false
- `coverNote` (string, ≀500) β€” message body
- `recipientName` (≀50), `subject` (≀200), `senderCompany` (≀100), `senderPhone` (validated E.164)

**Notifications:**

- `sendEmail` (boolean) β€” whether FaxDrop should email the sender a per-fax delivery confirmation. **Defaults to `false`** (suppress) β€” the MCP is built for batch/agent workflows where one email per fax floods the inbox, and the same information is already retrievable via `faxdrop_get_fax_status`. Set to `true` to opt back in. Failed-fax emails, operator alerts, refunds and status pages are unaffected. The send response echoes `deliveryEmail: 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_limited` with `retry_after` and `X-RateLimit-*` headers.
- **Account credit balance** β€” `402 payment_required` when 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](https://www.faxdrop.com/for-developers) 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](https://github.com/klodr/gmail-mcp) β€” Gmail
- πŸ“  [klodr/faxdrop-mcp](https://github.com/klodr/faxdrop-mcp) β€” Send real faxes via FaxDrop (you are here)
- 🏦 [klodr/mercury-invoicing-mcp](https://github.com/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).

TDQS

A4.8/5.0

Scored across 3 tools

Disambiguation5/5

Each tool has a distinct purpose: sending, whitelisting, and status checking. No overlap or ambiguity.

Naming Consistency5/5

All tools follow a consistent pattern of 'faxdrop_verb_noun' with snake_case naming, making the set predictable.

Tool Count4/5

With 3 tools, the server covers core fax operations without unnecessary bloat. While minimal, it suits a focused MCP server.

Completeness4/5

Essential operations (send, whitelist, status) are covered. A cancel or list tool would improve completeness, but the current set handles primary use cases.

Maintenance

ActivitySlowing
ResponsivenessNo issues