phishfort-mcp
```text
██████╗ ██╗ ██╗██╗███████╗██╗ ██╗███████╗ ██████╗ ██████╗ ████████╗
██╔══██╗██║ ██║██║██╔════╝██║ ██║██╔════╝██╔═══██╗██╔══██╗╚══██╔══╝
██████╔╝███████║██║███████╗███████║█████╗ ██║ ██║██████╔╝ ██║
██╔═══╝ ██╔══██║██║╚════██║██╔══██║██╔══╝ ██║ ██║██╔══██╗ ██║
██║ ██║ ██║██║███████║██║ ██║██║ ╚██████╔╝██║ ██║ ██║
╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝
███╗ ███╗ ██████╗██████╗
████╗ ████║██╔════╝██╔══██╗
██╔████╔██║██║ ██████╔╝
██║╚██╔╝██║██║ ██╔═══╝
██║ ╚═╝ ██║╚██████╗██║
╚═╝ ╚═╝ ╚═════╝╚═╝
MCP server + paired agent skill for PhishFort workflows
approval-gated writes | secret-safe defaults | no URL fetching
```
# phishfort-mcp




**A security-first MCP server and paired agent skill for the PhishFort Unified Client API.**
**Security-reviewed and hardened — 2026-07-29.** Approval-gated writes · secret-safe by default · no incident-URL fetching · every security claim verified against the code.
Bring PhishFort incident review, reporting, attachments, comments, and webhook management into your MCP client, then give your agent the workflow playbook for using those tools safely.
[Paired skill](https://github.com/mychaelconnolly/phishfort-mcp/blob/v0.1.1/skills/phishfort-mcp/SKILL.md) | [Official PhishFort API docs](https://capi.phishfort.com/) | [Security review](https://github.com/mychaelconnolly/phishfort-mcp/blob/v0.1.1/docs/reference/mcp-security-review.md) | [Local reference](https://github.com/mychaelconnolly/phishfort-mcp/blob/v0.1.1/docs/reference/phishfort-unified-client-api.md)
> Unofficial project. Not affiliated with, endorsed by, or maintained by PhishFort.
## Security, reviewed in the open
This is a security tool, so the security work is the headline — not a disclaimer at the bottom. Every control is implemented in code, covered by tests, and was put through a review-and-harden pass whose results are public.
- **2026-06-02 — v0.1.0.** Local `stdio` MCP server and paired agent skill, security-first by default: approval-gated writes, API host pinning, redirects disabled, and no fetching of incident URLs.
- **2026-06-05 — Posture mapped to evidence.** Every security feature documented against MCP, OpenAI, and Anthropic guidance, each row tied to the exact code and test behind it.
- **2026-06-10 — Review and hardening pass.** A security review conducted by Claude Fable 5 Ultracode (initial pass), Opus 4.8 xhigh, rubber-ducked with Codex 5.5 xhigh via plugin drove a hardening pass:
- destructive confirmation is now enforced on incident-action requests — the annotation and the approval gate agree,
- secret-named fields are scrubbed recursively, and one-time webhook secrets are written through `O_NOFOLLOW` `0600` files,
- reads retry bounded transport failures; writes retry only proven pre-send
connection/pool failures; secret-named fields are redacted from errors,
- the approval salt is random and process-stable, and the retry count is bounded,
- the webhook URL preflight rejects legacy numeric-IP encodings,
- the final attachment descriptor is held through upload, narrowing the
validate→upload replacement window.
The same pass **reworded every security claim to match what the code enforces** — the approval gate is described as in-process integrity and confirmation, not independent authorization, and the webhook URL check as a pre-submit sanity check. Tests went from 26 to 36.
- **2026-07-30 — v0.1.1.** Packages reference resources into wheels, adds current
Users, Clients, Documents, and total-count reads, distinguishes safe read retries from
mutation outcomes needing manual review, binds immutable attachment snapshots to
approval, validates webhook timestamp freshness, requires file-backed verifier secrets,
hardens one-time secret persistence beneath an owned `0700` root, and uses documented
attachment MIME types. Distribution uses a versioned Git tag and GitHub Release assets;
this project is not published to PyPI.
The table below is the evidence: each control maps to the code and the test that backs it.
## Standards-Backed Security Posture
This server was designed against the [Official Model Context Protocol security guidance](https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices), [Anthropic connector guidance](https://claude.com/docs/connectors/building/mcp), [OpenAI MCP guidance](https://developers.openai.com/api/docs/mcp), [OpenAI agent safety guidance](https://developers.openai.com/api/docs/guides/agent-builder-safety), and [PhishFort's official API docs](https://capi.phishfort.com/). The table below lists only security features that are implemented in code, with local evidence.
Local evidence:
- [MCP security review](https://github.com/mychaelconnolly/phishfort-mcp/blob/v0.1.1/docs/reference/mcp-security-review.md)
- [PhishFort API reference](https://github.com/mychaelconnolly/phishfort-mcp/blob/v0.1.1/docs/reference/phishfort-unified-client-api.md)
- [Source manifest](https://github.com/mychaelconnolly/phishfort-mcp/blob/v0.1.1/docs/reference/source-manifest.json)
| Security feature | What it prevents | Confirmed implementation |
| --- | --- | --- |
| Local `stdio` transport only | Avoids exposing a public HTTP MCP surface in v1 | `server.main()` rejects non-`stdio` transport |
| Two-step approval gate for writes | Forces an explicit plan→confirm step with a tamper-evident digest before any mutation. This is in-process integrity/confirmation, **not** independent human authorization — the host UI provides the human prompt via the destructive hints below | Write tools require `approval_id`, `approval_phrase`, `expires_at`, `request_digest`; `_validate_approval()` recomputes the digest from the actual params |
| Tamper-resistant approval digest | Blocks changing params after approval planning | `approval.py` canonicalizes params and verifies `request_digest`; covered by `test_approval_rejects_tampered_params` |
| Destructive confirmation | Adds explicit friction for delete/rotate operations | Destructive specs require `destructive_confirmed=true`; covered by `test_destructive_operation_requires_confirmed` |
| Read/write MCP annotations | Gives MCP hosts correct safety hints | `_read_annotations()` and `_write_annotations()` set read-only/destructive/idempotent hints |
| API keys never passed as tool args | Reduces credential leakage through prompts/tool logs | `Settings` reads `PHISHFORT_API_KEY` or `PHISHFORT_API_KEY_FILE`; tool signatures do not accept API keys |
| Default API host pinning | Avoids accidental credential use against arbitrary hosts | `Settings.validate_base_url()` requires `https://capi.phishfort.com/v1` unless explicit override is enabled |
| Redirects disabled | Avoids following API responses to unexpected locations | `httpx.AsyncClient(..., follow_redirects=False)` |
| Error redaction | Prevents API keys, secret-named fields, and echoed secret values from leaking through raised API errors | `PhishFortClient._error()` applies two-pass `redact()` masking; covered by API-key, sensitive-key, and echoed-value tests |
| Untrusted data warnings | Reminds agents not to treat remote content as instructions | `response_envelope()` adds `untrusted_data_warning` to PhishFort API outputs |
| No generic URL fetching | Avoids browsing hostile or sensitive URLs returned by the API | Server exposes PhishFort API tools only; no tool fetches incident or document URLs |
| Attachment file restrictions | Reduces local file exfiltration and approval-time file swaps | `snapshot_attachments()` reads bytes during planning and execution; `attachment_manifest()` binds planned path, size, SHA-256, and MIME; execution must match that manifest, then uploads the exact execution snapshot using the documented MIME map, including `application/vnd.ms-outlook` for `.msg` |
| Webhook URL preflight (defense-in-depth) | Rejects localhost/private/reserved targets — including legacy decimal/octal/hex IP forms — before a webhook is registered. The server never fetches the URL itself (PhishFort delivers webhooks), so backend egress controls remain the real boundary | `validate_webhook_url()` and `is_private_host()`; covered by webhook URL tests |
| Webhook secret containment | Keeps one-time secrets out of tool output and avoids post-mutation sink setup | `_handle_secret_response()` strips sensitive fields and echoes; `prepare_secret_write()` opens a bound sink beneath an owned `0700` root before mutation and `PreparedSecretWrite.persist()` installs the `0600` secret or reports a verified recovery path |
| Webhook signature verification | Enables receiver-side HMAC verification and rejects stale timestamps | `verify_signature()` signs the exact raw body, requires a numeric timestamp within ±300 seconds, and uses `hmac.compare_digest()`; receivers reject duplicate attempt delivery IDs and make logical-event processing idempotent |
| Limit-aware behavior | Avoids known API limit failures where possible | `phishfort_get_limits`, `reference_limits`, incident limit clamp, webhook 5-subscription preflight; covered by `tests/test_limits.py` |
| Bounded retry behavior | Avoids unsafe retry storms and duplicate writes | Reads may retry `429`/`5xx` with capped backoff and `Retry-After`; mutations retry only proven pre-send connection/pool failures and never status responses or ambiguous read/protocol failures; covered by read/write retry tests |
## About
`phishfort-mcp` is a public, unofficial MCP integration for teams and operators who want PhishFort incident workflows available inside agentic tools without giving up basic operational control. The MCP server provides live API access; the paired skill gives compatible agents the workflow memory needed to use that access consistently.
It is built for local-first use, explicit approvals, and careful handling of phishing data. The goal is not to make incident response fully autonomous. The goal is to make the repetitive parts faster while keeping sensitive actions, secrets, and untrusted content under control.
## Why This Exists
PhishFort has a focused REST API for phishing incident workflows. MCP makes that API usable from agentic tools, and the paired skill teaches those agents the operating procedure: what to read first, how to plan writes, what data is untrusted, and when to stop for explicit approval.
That pairing matters because security workflows are not just API calls. Incident data can contain hostile text, URLs should not be fetched casually, and takedown or webhook operations should not happen from a loose prompt.
`phishfort-mcp` ships two pieces that work together:
- a local `stdio` MCP server for live PhishFort API access
- an agent-agnostic skill that turns raw tool access into repeatable, safer workflows
- approval-gated writes for reporting, actions, evidence, comments, and webhooks
- secret-safe handling for API keys and one-time webhook secrets
- untrusted-data guardrails for incident text, URLs, and webhook payloads
## What You Can Do
| Workflow | Tools |
| --- | --- |
| Give agents the PhishFort operating playbook | `skills/phishfort-mcp/SKILL.md` |
| Check documented API limits | `phishfort_get_limits` |
| Check identity and client scope | `phishfort_whoami` |
| List users and clients in authorized scope | `phishfort_list_users`, `phishfort_list_clients` |
| Search and inspect incidents | `phishfort_list_incidents`, `phishfort_get_incident`, `phishfort_find_incident_by_subject` |
| List document metadata and obtain short-lived signed URLs | `phishfort_list_documents`, `phishfort_get_document`, `phishfort_get_document_signed_url` |
| Report URLs, domains, emails, phones, and IPv4 subjects | `phishfort_report_incident` |
| Request takedown, monitoring, or safe review | `phishfort_request_incident_action` |
| Add evidence and analyst context | `phishfort_add_attachments`, `phishfort_add_comment` |
| Manage webhook subscriptions | `phishfort_list_webhooks`, `phishfort_create_webhook`, `phishfort_update_webhook`, `phishfort_delete_webhook`, `phishfort_test_webhook`, `phishfort_rotate_webhook_secret` |
| Verify incoming webhook deliveries | `phishfort_verify_webhook_signature` |
The server also exposes MCP resources for the distilled API reference, source manifest, and security review:
- `phishfort://reference/summary`
- `phishfort://reference/limits`
- `phishfort://reference/source-manifest`
- `phishfort://reference/security-review`
## Paired Skill
This repo ships an agent-agnostic skill in [skills/phishfort-mcp/SKILL.md](https://github.com/mychaelconnolly/phishfort-mcp/blob/v0.1.1/skills/phishfort-mcp/SKILL.md). Use it with any skill-capable MCP host to teach the agent the safe operating pattern for this server: read before write, treat all remote API data as untrusted, never fetch returned URLs by default, and use `phishfort_plan_change` before mutating calls.
The skill keeps detailed workflows in [references/workflows.md](https://github.com/mychaelconnolly/phishfort-mcp/blob/v0.1.1/skills/phishfort-mcp/references/workflows.md), exact tool parameters in [references/tool-map.md](https://github.com/mychaelconnolly/phishfort-mcp/blob/v0.1.1/skills/phishfort-mcp/references/tool-map.md), and points agents to `phishfort_get_limits` before workflows where limits change the right next step.
The GitHub Release wheel installs the MCP server and its embedded reference resources.
The paired skill is a repository asset; download it from the matching Git tag when your
host supports skills. This project is not published to PyPI.
## Safety Built In
The standards-backed table above is the detailed proof. Operationally, the server stays local-first, keeps credentials out of tool arguments, treats PhishFort data as untrusted, gates writes through `phishfort_plan_change`, stores webhook secrets outside tool output, and constrains attachments, webhook URLs, limits, and retries.
See [MCP security review](https://github.com/mychaelconnolly/phishfort-mcp/blob/v0.1.1/docs/reference/mcp-security-review.md) for the reasoning behind these choices.
## Quick Start
```bash
uvx --from git+https://github.com/mychaelconnolly/phishfort-mcp.git@v0.1.1 phishfort-mcp --help
```
For source development or the paired skill:
```bash
git clone --branch v0.1.1 --depth 1 https://github.com/mychaelconnolly/phishfort-mcp.git
cd phishfort-mcp
uv sync --frozen --extra dev
uv run phishfort-mcp --help
```
Create a local key file:
```bash
mkdir -p ~/.config/phishfort-mcp
chmod 700 ~/.config/phishfort-mcp
$EDITOR ~/.config/phishfort-mcp/phishfort-api-key.txt
chmod 600 ~/.config/phishfort-mcp/phishfort-api-key.txt
```
## Codex MCP Registration
```bash
codex mcp add phishfort \
--env PHISHFORT_API_KEY_FILE=$HOME/.config/phishfort-mcp/phishfort-api-key.txt \
-- uvx --from git+https://github.com/mychaelconnolly/phishfort-mcp.git@v0.1.1 phishfort-mcp
```
Then verify:
```bash
codex mcp list
```
A fresh Codex session may be required before new MCP tools are discoverable.
## Configuration
| Variable | Default | Notes |
| --- | --- | --- |
| `PHISHFORT_API_BASE_URL` | `https://capi.phishfort.com/v1` | Pinned to official API host unless override is enabled. |
| `PHISHFORT_API_KEY` | unset | Useful for short-lived local shells. |
| `PHISHFORT_API_KEY_FILE` | unset | Preferred for MCP registration. |
| `PHISHFORT_SECRET_DIR` | `~/.config/phishfort-mcp/secrets` | Effective-user-owned `0700` root; webhook secrets are written with `0600` permissions. |
| `PHISHFORT_ATTACHMENT_ROOTS` | `.` | Comma-separated roots allowed for attachment uploads. |
| `PHISHFORT_TIMEOUT_SECONDS` | `30` | HTTP request timeout. |
| `PHISHFORT_MAX_RETRIES` | `3` | Caps read-side `429`/`5xx` retries and mutation retries for proven pre-send connection/pool failures at 5. Mutation status responses and ambiguous read/protocol failures are not retried; read-side `Retry-After` is capped locally. |
| `PHISHFORT_ALLOW_CUSTOM_BASE_URL` | `false` | Test-only escape hatch for non-production API hosts. |
| `PHISHFORT_ALLOW_UNSAFE_WEBHOOK_URL` | `false` | Test-only escape hatch for localhost/private webhook targets. |
`phishfort_verify_webhook_signature` accepts `secret_file` only. The file must remain
beneath `PHISHFORT_SECRET_DIR`; the root and any subdirectories must be owned by the
effective user with mode `0700`. Symlinked components, root rebinding, and platforms
without required descriptor-relative no-follow primitives fail closed.
## Approval-Gated Writes
Read tools can be called directly. Writes are two-step on purpose:
1. Call `phishfort_plan_change` with `operation` and exact params.
2. Review `warnings`, `risk`, `request_digest`, and `approval_phrase`.
3. Call the intended mutating tool with the same params plus `approval_id`, `approval_phrase`, `expires_at`, and `request_digest`.
If anything changes, rerun `phishfort_plan_change`.
This gate is in-process integrity and confirmation: it proves the executed params
match the planned params (tamper-evident digest), enforces expiry, and requires
`destructive_confirmed=true` for destructive operations. It is **not** an independent
authorization boundary — the same agent can plan and confirm. The human-in-the-loop
checkpoint is the MCP host's own tool-confirmation UI, driven by the destructive
annotations the server sets.
Mutating API calls are not automatically replayed after HTTP status responses or ambiguous
read/protocol failures. PhishFort documents no idempotency key for incident reports. A
report `409` means duplicate. Any `2xx` with a nonempty incident `id` confirms creation,
even when dashboard `url` is null or absent; a `2xx` without a nonempty `id` stops for
manual review with no automatic retry.
## Verification
```bash
uv run ruff check .
uv run pytest
```
Optional live smoke when a valid key exists:
- `phishfort_whoami`
- `phishfort_list_incidents(limit=1)`
Do not run live mutating smoke unless you intend to change PhishFort state.
Document signed URLs are short-lived download grants. Treat them as sensitive, untrusted
API output: do not persist or share them, and do not fetch them automatically.
## API Reference
Official PhishFort docs:
- [Introduction](https://capi.phishfort.com/)
- [Authentication](https://capi.phishfort.com/auth/)
- [Limits](https://capi.phishfort.com/limits/)
- [Incident Lifecycle](https://capi.phishfort.com/incident-lifecycle/)
- [Official Docs MCP](https://capi.phishfort.com/mcp-server/)
- [List Incidents](https://capi.phishfort.com/incidents/)
- [Single Incident](https://capi.phishfort.com/single-incident/)
- [Report Incident](https://capi.phishfort.com/report-incident/)
- [Request Incident Action](https://capi.phishfort.com/request-incident-review/)
- [Add Attachments](https://capi.phishfort.com/add-attachments/)
- [Add Comment](https://capi.phishfort.com/add-comment/)
- [List Users](https://capi.phishfort.com/list-users/)
- [List Clients](https://capi.phishfort.com/clients/)
- [Documents](https://capi.phishfort.com/documents/)
- [Webhooks](https://capi.phishfort.com/webhooks/)
- [Zendesk Integration](https://capi.phishfort.com/zendesk/)
- [Data Structures](https://capi.phishfort.com/structures/)
- [OpenAPI Reference](https://capi.phishfort.com/reference/)
PhishFort's docs MCP serves documentation, not account API operations. This project remains
the local account-workflow MCP server.
This repo includes a distilled reference in [docs/reference/phishfort-unified-client-api.md](https://github.com/mychaelconnolly/phishfort-mcp/blob/v0.1.1/docs/reference/phishfort-unified-client-api.md) and a source URL manifest in [docs/reference/source-manifest.json](https://github.com/mychaelconnolly/phishfort-mcp/blob/v0.1.1/docs/reference/source-manifest.json). Fetched raw PhishFort docs are intentionally not tracked.
## License
MIT. See [LICENSE](https://github.com/mychaelconnolly/phishfort-mcp/blob/v0.1.1/LICENSE).
TDQS
Scored across 23 tools
Each tool targets a distinct operation: incident listing vs. finding by subject, document metadata vs. signed URL, etc. No two tools have overlapping purposes; descriptions clearly differentiate them.
All tools follow a consistent 'phishfort_verb_noun' pattern in snake_case, e.g., 'list_incidents', 'create_webhook', 'verify_webhook_signature'. No mixing of conventions.
23 tools is on the higher side but still reasonable for a platform covering incidents, documents, users, clients, webhooks, and system info. Each tool has a clear purpose, so the count feels justified rather than excessive.
Core incident lifecycle is covered (list, get, report, request action, add attachments/comments), but missing direct update or delete incident operations. Webhook CRUD is complete. Overall, notable gaps exist but the main workflows are functional.