# sec-mcp
**Check domains, URLs and IP addresses against live security blacklists — as a Python library, a terminal CLI, or an MCP server that gives LLM agents real-time threat context.**
sec-mcp aggregates ten public threat-intelligence feeds into a local SQLite database and answers "is this domain/URL/IP malicious?" in microseconds. Use it inside any Python application, from the terminal with `sec-mcp`, or as a [Model Context Protocol](https://modelcontextprotocol.io/) server so tools like Claude Desktop, Cursor and Windsurf can screen indicators against real blacklist data.
Developed by [Montimage](https://www.montimage.eu), a company specializing in cybersecurity and network monitoring solutions.
---
## Table of Contents
- [Features](#features)
- [Demo](#demo)
- [Quick Start](#quick-start)
- [Installation](#installation)
- [Usage](#usage)
- [CLI](#cli)
- [Python API](#python-api)
- [MCP Server](#mcp-server)
- [Performance Optimization](#performance-optimization)
- [Benchmarking](#benchmarking)
- [Configuration](#configuration)
- [Project Structure](#project-structure)
- [Documentation](#documentation)
- [Contributing](#contributing)
- [Related Publications](#related-publications)
- [License](#license)
- [Acknowledgments](#acknowledgments)
---
## Features
- **Comprehensive security checks** — validate domains, URLs and IPv4/IPv6 addresses, with domain→URL cascade semantics (a blacklisted domain condemns its URLs and subdomains; a blacklisted URL does not condemn its domain) and CIDR-range matching for IPs
- **Ten threat-intelligence feeds** — OpenPhish, PhishStats, URLhaus, PhishTank, Spamhaus DROP, Dshield, CINSSCORE, EmergingThreats, FeodoTracker and BlocklistDE
- **Three interfaces, one engine** — Python API (`SecMCP`), Click CLI (`sec-mcp`), and an MCP server (`sec-mcp-server`, stdio or streamable HTTP) exposing six typed tools
- **Two storage backends** — default SQLite storage (v1), or an in-memory hybrid (v2, `MCP_USE_V2_STORAGE=true`) with O(1) per-type indexes; up to ~28,000x faster lookups than the database-only path on the bundled benchmark
- **URL normalization** — one canonicalizer shared by both backends: lowercasing, default-scheme handling and tracking-parameter stripping (`utm_*`, `fbclid`, …), so URL variants share one verdict
- **Safe feed ingestion** — HTTPS-only downloads bounded by `max_feed_bytes` and entry-count sanity checks; a failing feed never aborts the others
- **Scheduled daily updates** — a `schedule` job refreshes feeds at `update_time`; forced updates are rate limited to one per `min_update_interval_seconds` (default 300 s)
- **Concurrency-friendly** — SQLite in WAL mode with `synchronous=NORMAL`, a thread-local shared connection per check, and lock-free snapshot reads on the v2 backend
- **Structured MCP output** — every tool publishes a typed `outputSchema`, results carry `structuredContent`, `check_batch` reports a tri-state verdict (`safe` / `blacklisted` / `invalid`), and domain failures surface as `isError` results instead of protocol crashes
- **Observable** — `get_diagnostics` MCP tool (summary / full / health / performance / sample modes), real `scheduler_alive` reporting and per-source entry counts
---
## Demo
> **PLACEHOLDER — needs maintainer input before publishing.** The repository ships no
> screenshot or demo assets today. Suggested: an asciinema recording of
> `sec-mcp update` → `sec-mcp check `, or a screenshot of the six MCP tools
> inside Claude Desktop. Add the file under `docs/assets/` (e.g. `docs/assets/demo.gif`)
> and replace this block with ``.
---
## Quick Start
From zero to a verdict in under five minutes:
```bash
pip install sec-mcp
sec-mcp update # download + index the feeds (first run only)
sec-mcp check https://example.com # → Status: Safe / Blacklisted
sec-mcp status # entry counts + per-source breakdown
```
To run it as an MCP server instead, jump to [MCP Server](#mcp-server) — no install beyond `uvx` is needed.
---
## Installation
```bash
pip install sec-mcp
```
### Requirements
- Python 3.11 or newer (CI tests 3.11–3.14)
- SQLite 3 (bundled with Python)
- Optional: `pytricia` via `pip install "sec-mcp[fast-cidr]"` for fast CIDR matching; without it a pure-Python `ipaddress` matcher is used (`pytricia` builds from source and needs a C compiler)
### From source
Requires [uv](https://docs.astral.sh/uv/getting-started/installation/) (`pip install uv` if needed):
```bash
git clone https://github.com/Montimage/sec-mcp.git
cd sec-mcp
uv sync # creates .venv, installs the package editable + dev tools, pinned by uv.lock
```
---
## Usage
### CLI
```bash
# Single check — auto-detects domain vs URL vs IP
sec-mcp check https://example.com
sec-mcp check malicious-domain.com
sec-mcp check 192.168.1.1
# Type-specific checks
sec-mcp check-domain example.com
sec-mcp check-url https://example.com/path
sec-mcp check-ip 192.168.1.1
# Batch check from a file (one value per line)
sec-mcp batch urls.txt
# Status, updates, sampling and cache control
sec-mcp status
sec-mcp update
sec-mcp sample -n 20
sec-mcp flush-cache
```
Every command except `sample` accepts `--json` for machine-readable output.
### Python API
```python
from sec_mcp import SecMCP
client = SecMCP() # optional: SecMCP(db_path="/path/to/mcp.db")
client.update() # download + index the feeds (first run only)
# Single check — check() returns CheckResult(blacklisted, explanation)
result = client.check("https://example.com")
print(result.blacklisted, result.explanation)
# result.to_dict() -> {"is_safe": True, "explain": "Not blacklisted"}
# Type-specific checks
client.check_domain("example.com")
client.check_url("https://example.com/path")
client.check_ip("192.168.1.1")
# Batch check
urls = ["https://example.com", "https://test.com", "192.168.1.1"]
for value, r in zip(urls, client.check_batch(urls)):
print(f"{value}: {'BLOCKED' if r.blacklisted else 'SAFE'}")
# Status — StatusInfo(entry_count, last_update, sources, server_status)
status = client.get_status()
print(status.entry_count, status.last_update, client.scheduler_alive())
```
### MCP Server
sec-mcp runs as a stdio MCP server for AI/LLM integration (Claude Desktop, Cursor, Windsurf, …). `server.json` at the repo root ships the matching MCP Registry entry (`io.github.montimage/sec-mcp`).
1. Install `uv` (provides `uvx`): see the [uv installation docs](https://docs.astral.sh/uv/getting-started/installation/), or `pip install uv`.
2. Populate the blacklist once:
```bash
uvx --from sec-mcp sec-mcp update
```
3. Configure your MCP client (e.g. `claude_desktop_config.json`):
```json
{
"mcpServers": {
"sec-mcp": {
"command": "uvx",
"args": ["--from", "sec-mcp", "sec-mcp-server"],
"env": { "MCP_USE_V2_STORAGE": "true" }
}
}
}
```
> **How it works**: `uvx` downloads the `sec-mcp` package from PyPI and runs its
> `sec-mcp-server` entry point — no virtual environment or absolute Python path
> required. Alternatively, `pip install sec-mcp` into any environment and use
> `"command": "sec-mcp-server"` with no `args`.
#### MCP tools
| Tool | Description |
|---------------------|--------------------------------------------------------------------------|
| `check_batch` | Check multiple domains/URLs/IPs in one call |
| `get_status` | Blacklist status: entry counts, per-source breakdown, `scheduler_alive` |
| `update_blacklists` | Force an immediate update (rate limited; emits per-source progress) |
| `get_diagnostics` | Diagnostic info — modes: `summary`, `full`, `health`, `performance`, `sample` |
| `add_entry` | Manually add a blacklist entry (URL/domain or IP, optional score) |
| `remove_entry` | Remove a blacklist entry by domain, URL or IP |
`update_blacklists` is rate limited to one forced update per `min_update_interval_seconds` (default 300 in `config.json`): a second call inside the window returns `{"updated": false, "reason": ...}` without starting downloads, and callers that supply a progress token receive one `notifications/progress` per source.
The `get_diagnostics` modes:
- **`summary`** (default): entry counts, sources and last update times
- **`full`**: complete diagnostic data including health, stats and performance
- **`health`**: database reachability and scheduler state only
- **`performance`**: lookup metrics and hit rates (v2 storage only)
- **`sample`**: random sample of blacklist entries (`sample_count` parameter, 1–100)
#### HTTP transport and the landing-page playground
`sec-mcp-server` speaks stdio by default. Add `--http` to serve MCP over
streamable HTTP instead, so a browser — including the live playground on the
[landing page](https://montimage.github.io/sec-mcp/) — can call the tools:
```bash
sec-mcp update # populate the database once
sec-mcp-server --http # → http://127.0.0.1:8000/mcp
sec-mcp-server --http --host 0.0.0.0 --port 9000
```
- CORS allows the origins in `SEC_MCP_CORS_ORIGINS` (default: local Vite dev/preview
on ports 3000/4173 and `https://montimage.github.io`); credentials are never allowed.
`*` allows any origin and also disables DNS-rebinding protection.
- Set `SEC_MCP_HTTP_AUTH_TOKEN` to require `Authorization: Bearer ` on every
request. Binding a non-loopback host (e.g. `--host 0.0.0.0`) without one generates
a random token for that run and prints it with a `#token=…` fragment: append it to
the landing-page URL and the playground connects with it. Pass `--no-auth` to opt
out (a warning is logged).
---
## Performance Optimization
Enable the in-memory v2 backend for dramatically faster lookups:
```bash
export MCP_USE_V2_STORAGE=true
```
### Measured results
Numbers below come from the bundled harness (`run_benchmark.sh`, see [Benchmarking](#benchmarking)):
| Operation | v1 (Database) | v0.3.0 (Hybrid) | v0.4.0+ (Optimized) | Speedup (vs v1) |
|------------------|---------------|-----------------|---------------------|-----------------|
| Domain check | 10 ms | 0.01 ms | **0.006 ms** | **~1,600x** |
| URL check | 5 ms | 0.001 ms | **0.0007 ms** | **~7,000x** |
| IP + CIDR check | 200 ms | 0.01 ms | **0.007 ms** | **~28,000x** |
| Batch 100 items | 2–3 s | 50–100 ms | **50–100 ms** | **~30x** |
### Memory usage
- **v1 (default)**: ~10 MB (database on disk)
- **v2**: ~40–80 MB in-memory, depending on feed size
### How v2 is fast
1. **One in-memory index per entry type** — O(1) hash lookups over single domain/URL/IP indexes; reload builds a scratch index and swaps it under the lock, so readers never block and always see a complete snapshot.
2. **URL normalization** — `HTTP://EVIL.COM/` → `http://evil.com`; tracking parameters (`utm_*`, `fbclid`, …) stripped. One canonicalizer shared by both backends means identical verdicts.
3. **Integer IPv4 storage** — 4 bytes per IP instead of 13+ as a string.
4. **CIDR matching** — `pytricia` when the `fast-cidr` extra is installed, pure-Python `ipaddress` otherwise.
### Monitoring performance
```python
# v2 storage only — the get_diagnostics MCP tool exposes the same data
metrics = client.storage.get_metrics()
# {"total_lookups": ..., "cache_hits": ..., "hit_rate": ...,
# "avg_lookup_time_ms": ..., "memory_usage_mb": ..., "using_pytricia": ...}
```
### Rollback to v1
```bash
export MCP_USE_V2_STORAGE=false # or unset MCP_USE_V2_STORAGE
```
---
## Benchmarking
Compare storage backends with the bundled harness (requires `pip install "sec-mcp[fast-cidr]"` for the full comparison; `psutil` ships with sec-mcp):
```bash
./run_benchmark.sh --quick # 10K entries, ~30 s
./run_benchmark.sh # 50K entries, ~2 min
./run_benchmark.sh --full --memory # 100K entries + memory profiling
./run_benchmark.sh --v1 --v2opt # compare specific versions
./run_benchmark.sh --all # all versions
```
| Flag | Description |
|-----------|--------------------------------------------------------|
| `--quick` | Quick benchmark — 10K entries, 500 iterations |
| `--full` | Full benchmark — 100K entries, 1000 iterations |
| `--all` | Compare all versions (v1, v0.3.0, v0.4.0+) |
| `--v1` | Database-only storage |
| `--v2` | Hybrid storage |
| `--v2opt` | Optimized hybrid storage |
| `--memory`| Include memory profiling (requires psutil) |
Methodology and expected output: [BENCHMARK_PLAYBOOK.md](BENCHMARK_PLAYBOOK.md).
---
## Configuration
### Environment variables
| Variable | Description | Default |
|-------------------------|------------------------------------------------------------|--------------------------------|
| `MCP_DB_PATH` | Custom SQLite database location | Platform-specific (see below) |
| `MCP_USE_V2_STORAGE` | `true` selects the in-memory `HybridStorage` backend | `false` (v1 storage) |
| `MCP_LOG_PATH` | Log file location | platformdirs log dir |
| `MCP_CACHE_DIR` | Feed download cache directory | platformdirs cache dir |
| `MCP_DISABLE_SCHEDULER` | Set to `1` to not start the daily-update scheduler thread | unset (scheduler runs) |
| `SEC_MCP_CORS_ORIGINS` | Comma-separated CORS origins for `sec-mcp-server --http`; `*` = any | local Vite ports + GitHub Pages |
| `SEC_MCP_HTTP_AUTH_TOKEN` | Bearer token required by `sec-mcp-server --http` | unset (generated per run on a non-loopback host) |
### Default database locations
- **macOS**: `~/Library/Application Support/sec-mcp/mcp.db`
- **Linux**: `~/.local/share/sec-mcp/mcp.db`
- **Windows**: `%APPDATA%\sec-mcp\mcp.db`
### Configuration file
The shipped [`sec_mcp/config.json`](sec_mcp/config.json) controls feed sources, the update schedule and feed-safety bounds:
```json
{
"blacklist_sources": { "OpenPhish": "...", "PhishStats": "...", "URLhaus": "...", "...": "10 feeds total" },
"update_time": "00:00",
"min_update_interval_seconds": 300,
"cache_size": 10000,
"max_feed_bytes": 67108864,
"min_feed_entries": 1,
"max_feed_entries": 500000,
"max_range_addresses": 65536,
"log_level": "INFO"
}
```
`update_time` sets the daily update hour, `min_update_interval_seconds` rate-limits forced updates, `cache_size` bounds the v1 positive-hit LRU, and the `max_*`/`min_*` keys bound accepted feed sizes.
---
## Project Structure
```
sec-mcp/
├── sec_mcp/ # The package
│ ├── __init__.py # Public API: SecMCP, CheckResult, StatusInfo, __version__
│ ├── sec_mcp.py # SecMCP facade (check/check_batch/update/get_status/…)
│ ├── cli.py # `sec-mcp` CLI (click)
│ ├── mcp_server.py # Six MCP tools (SDK 2.x MCPServer)
│ ├── start_server.py # `sec-mcp-server` entry point (stdio, or --http)
│ ├── http_transport.py # Streamable-HTTP app: CORS, DNS-rebinding, bearer auth
│ ├── storage.py # Backend selector + v1 database-only storage
│ ├── storage_queries.py # v1 read/query half
│ ├── storage_base.py # Shared schema, DB-path resolution, normalize_url
│ ├── storage_v2.py # v2 HybridStorage (in-memory indexes + metrics)
│ ├── storage_v2_db.py # v2 persistence layer (all SQLite access)
│ ├── storage_v2_index.py # v2 in-memory indexes + CIDR matching
│ ├── storage_v2_writes.py # v2 write path (persistence + rollback)
│ ├── storage_v2_stats.py # v2 stats/reporting mixin
│ ├── feed_parsers.py # One parser per blacklist feed source
│ ├── update_blacklist.py # Feed download, scheduling and ingestion
│ ├── utility.py # Validation, logging, config
│ ├── config.json # Shipped defaults (sources, schedule, bounds)
│ └── tests/ # pytest suite (pytest.ini sets testpaths)
├── docs/ # ARCHITECTURE, DEVELOPMENT, DEPLOYMENT, agent-env, playbook, decisions/, archive/
├── scripts/ # check_test_baseline.sh (CI floor gate)
├── .github/ # CI + PyPI + landing workflows, issue & PR templates
├── benchmark.py # Benchmark script
├── run_benchmark.sh # Benchmark helper
├── server.json # MCP Registry metadata
├── react-landing-page/ # Standalone Vite site (not part of the package)
├── CONTRIBUTING.md # Dev setup, tests, conventions, PR process
├── CODE_OF_CONDUCT.md # Contributor Covenant
├── SECURITY.md # Vulnerability reporting policy
├── CHANGELOG.md # Release history (Keep a Changelog)
├── LICENSE # Apache-2.0
└── README.md # This file
```
---
## Documentation
| Document | Contents |
|----------|----------|
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | Design overview: components, storage backends, check semantics, MCP layer |
| [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) | Dev-workflow hub: setup, repo layout, CI gates, conventions |
| [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) | PyPI release flow, end-user MCP-server deployment, landing-page deploy |
| [docs/agent-env.md](docs/agent-env.md) | Toolchain, environment variables, probe isolation |
| [docs/playbook.md](docs/playbook.md) | Top-10 usage cookbook for MCP clients |
| [docs/decisions/](docs/decisions/) | Architecture decision records (ADRs) |
| [BENCHMARK_PLAYBOOK.md](BENCHMARK_PLAYBOOK.md) | Benchmark methodology and interpretation |
| [CONTRIBUTING.md](CONTRIBUTING.md) | Dev setup, test/lint/coverage commands, branch & PR conventions |
| [SECURITY.md](SECURITY.md) | Supported versions and private vulnerability reporting |
| [CHANGELOG.md](CHANGELOG.md) | Release history |
---
## Contributing
Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for the dev setup (`uv sync` is the single install command), test and lint commands, and the branch/commit/PR conventions. All contributors are expected to follow the [Code of Conduct](CODE_OF_CONDUCT.md), and security issues should be reported privately per [SECURITY.md](SECURITY.md).
- **Issues**: [GitHub Issues](https://github.com/Montimage/sec-mcp/issues) — bug report and feature request templates provided
- **Email**: contact@montimage.eu
- **Website**: https://www.montimage.eu
---
## Related Publications
No publications referencing sec-mcp are known yet. If you use sec-mcp in academic work, talks or blog posts, please [open an issue](https://github.com/Montimage/sec-mcp/issues) so we can list it here.
---
## License
[Apache License 2.0](LICENSE) — Copyright 2026 [Montimage](https://www.montimage.eu).
---
## Acknowledgments
- Threat-intelligence providers: OpenPhish, PhishStats, URLhaus, PhishTank, Spamhaus, Dshield, CINSSCORE, EmergingThreats, FeodoTracker and BlocklistDE
- Built on the [Model Context Protocol](https://modelcontextprotocol.io/) Python SDK
- Powered by Python, SQLite, Click and httpx