Metadata-Version: 2.4
Name: cyberspf-mcp
Version: 0.2.0
Summary: Ask CyberSPF about a domain, IP or ASN from an MCP client.
Author-email: Trontech Inc <hello@cyberspf.com>
License-Expression: LicenseRef-Proprietary
Project-URL: Homepage, https://cyberspf.com
Project-URL: Documentation, https://cyberspf.com/api/v1/docs/
Keywords: mcp,security,dns,dmarc,spf,bgp,threat-intel
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: System Administrators
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Security
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: mcp==1.9.4
Requires-Dist: httpx==0.28.1
Requires-Dist: anyio==4.6.2.post1

<!-- mcp-name: com.cyberspf/mcp -->

# CyberSPF MCP server

Lets an MCP client — Claude Desktop, an editor, an agent framework — ask CyberSPF
about an IP, a domain or an ASN, list what an account monitors, and pull the
graded report card.

## What it is, and what it deliberately is not

It runs **on the customer's machine**, over stdio, calling `/api/v1/` over HTTPS
with their own API key.

That shape is the design, not a shortcut:

- **No new inbound surface.** The droplet's only ingress stays the Cloudflare
  tunnel. A hosted MCP endpoint would add a second one.
- **No new authentication.** `ClientAPIKeyAuthentication` and `HasApiAccess`
  already gate v1. This is another client of them, not a new path in.
- **No new metering.** `api_requests_per_day` already counts these calls.
- **Nothing to operate.** A process the client starts and stops.

A hosted HTTP server is worth building if customers ask for one. It is not worth
building first.

## Requires

A plan that includes **API access**, and for `cyberspf_report_card`, a plan that
includes **the report card**. Those are two separate entitlements and the server
reports them differently — a key that works for lookups and is refused for the
card is a plan limit, not a bad key.

## Install

Nothing to install — `uvx` fetches and runs it:

```bash
uvx cyberspf-mcp
```

Or into an environment you manage:

```bash
pip install cyberspf-mcp
```

From a checkout, for development:

```bash
cd mcp_server
python -m venv .venv
. .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install -r requirements.txt
```

## Configure

Create a key in CyberSPF under **Settings → Webhooks and API**, then point your client at
the server. For Claude Desktop, in `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "cyberspf": {
      "command": "uvx",
      "args": ["cyberspf-mcp"],
      "env": {
        "CYBERSPF_API_KEY": "your key here"
      }
    }
  }
}
```

That form exists because the alternative was two absolute paths. A client started
from a desktop launcher inherits no `PATH` and no working directory, and a relative
path there is the commonest reason a server appears in the client list and never
answers. If `uvx` itself is not on the launcher's `PATH`, give its full path.

From a checkout instead, point `command` at the venv's python and `args` at
`server.py` — both absolute, for the same reason.

## Listing

`server.json` is the manifest for the official MCP Registry
(`registry.modelcontextprotocol.io`), published as **`com.cyberspf/mcp`**. The
registry namespaces by reverse DNS and verifies the domain, which is why the name
is the one domain we can prove.

A stdio server is listed there through a `packages` entry naming a package registry
and an identifier — so **the PyPI package is a prerequisite for the listing**, not
merely a convenience.

`CYBERSPF_BASE_URL` overrides the endpoint; it defaults to
`https://cyberspf.com/api/v1`. Useful for pointing at a staging host and for
nothing else.

## The tools

| Tool | Answers |
|---|---|
| `cyberspf_investigate` | **start here** — looks one thing up and follows what it points at, in a single call |
| `cyberspf_lookup` | one IP, domain, CIDR or ASN — ordered sections of findings with severities |
| `cyberspf_list_monitors` | what this account watches, and when each was last checked |
| `cyberspf_report_card` | the graded assessment per monitored domain, with recommendations |
| `cyberspf_alerts` | what changed, newest first — page with `before`, poll with `since` |
| `cyberspf_threat_lists` | the account's threat lists, their tags and sizes |
| `cyberspf_tag_suggestions` | tags already in use, so a new one is not a near-miss of an old one |
| `cyberspf_bulk_lookup` | submit many terms at once; returns a job id |
| `cyberspf_bulk_result` | collect a bulk job — polling is free |

`cyberspf_investigate` and `cyberspf_lookup` both take `detail`: `summary`
returns only findings rated bad or warn plus a count of every severity seen,
which is far less to read. Investigate defaults to summary because it returns
several subjects at once; lookup defaults to full.

And these change the account:

| Tool | Does |
| --- | --- |
| `cyberspf_start_monitoring` | begin watching a domain, IP or ASN |
| `cyberspf_stop_monitoring` | stop watching one |
| `cyberspf_create_threat_list` | create an empty list, named, tagged and graded |
| `cyberspf_add_to_threat_list` | add hosts, IPs or CIDR blocks — up to 100 a call |
| `cyberspf_remove_from_threat_list` | take entries back out |

Each of those says `CHANGES THE ACCOUNT` in its own description, and the two
that remove protection — stopping a monitor, and un-flagging a threat — tell
the model to confirm with the person first. A monitor that was quietly stopped
does not announce itself: the next real change simply goes unreported.

It started as three, not the eight the design sketch proposed: two of those
eight (`zone_search` and anything over new-domain data) have no data behind
them — `zone_domain` holds zero rows since collection was paused — and the rest
wrapped endpoints v1 did not expose. Three that work beat eight where five
return errors.

Fourteen now, because v1 grew alerts, threat lists, monitor writes and
`investigate` on 2026-10-03. The rule did not change, only what satisfies it: a
tool ships when there is a working endpoint behind it. `zone_search` still has
none.

### Following a result instead of stopping at it

`cyberspf_lookup` returns the API's own sections unflattened, on purpose. Values
that can themselves be looked up carry a `type` (`ip`, `domain`, `asn`, `cidr`),
so an agent can feed one back in:

> look up `example.com` → take the addresses it resolves to → look each up →
> take the ASN announcing them → look that up

That chain is the point. Flattening the response into prose for the model would
read more smoothly and would throw away the one field that makes the chain
possible.

## Failure messages

Every failure is phrased as something a person can act on, because an agent
cannot read a stack trace and cannot retry its way out of a plan limit:

| Condition | What the tool says |
|---|---|
| no `CYBERSPF_API_KEY` | exits at startup, naming the setting — rather than appearing healthy and failing on first use |
| 401 | the key was rejected; check it has not been revoked |
| 403 | the key is valid, the plan does not include what was asked for |
| 429 | daily allowance used up, resets midnight UTC |
| timeout | 45s, and says a first lookup of a cold domain can be slow before concluding it is down |

A blank `term` is refused locally rather than sent, so it does not spend one of
the account's daily calls to be told it was blank.

## Nothing here probes anything

Every tool reads what CyberSPF already stored. None of them causes a connection
to the subject of the lookup, from the customer's machine or from ours — the
report card in particular is built from stored data, which is what makes it safe
for an agent to call in a loop. Outbound probing happens elsewhere in the
product, from the dedicated prober, on its own schedule.
