threatintel-mcp
Provides tools for looking up file hashes, domains, and IP addresses to retrieve multi-engine detection ratios, threat labels, and reputation context from VirusTotal.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@threatintel-mcp lookup ip 8.8.8.8"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
threatintel-mcp
An MCP server that wraps threat-intelligence lookup APIs — urlscan.io and VirusTotal — so an AI agent (e.g. Claude) can pivot on indicators during an investigation the way an analyst does at a terminal.
This is a portfolio demonstration. It is a compact, self-contained example built to show design judgment around MCP servers for threat intelligence: tool ergonomics, compact/structured output, rate-limit hygiene, OPSEC, and treating tool output as untrusted. It is not a production incident-response platform, and it wraps only a small slice of each provider's API.
Why this exists
When you investigate a suspicious indicator, you pivot: a URL leads to a domain, the domain resolves to an IP, the IP belongs to an ASN, a dropped file has a hash, that hash shows up on other scans. Each hop is an API call to a different threat-intel platform.
Exposing those platforms as MCP tools lets an agent do that pivoting autonomously and in natural language — "is this domain malicious, and what else lives on its hosting?" — while the human stays in the loop for judgment. MCP is the clean seam for this: one small server, any MCP-capable client.
Related MCP server: virustotal-mcp-server
What it does — the tools
Tool | Provider | Purpose |
| urlscan.io | Submit a URL for a live sandbox scan (returns a |
| urlscan.io | Poll a scan |
| urlscan.io | Passively search historical scans (Elasticsearch query syntax). |
| VirusTotal v3 | Multi-engine verdict + threat label for a file hash (MD5/SHA-1/SHA-256). |
| VirusTotal v3 | Reputation, registrar, creation date, categories for a domain. |
| VirusTotal v3 | Reputation + hosting context (ASN, owner, country) for an IP. |
Each tool's docstring is written as a "when to call me" prompt — the SDK turns the type hints and docstring into the JSON Schema and description the model sees, so the tools are self-documenting to the agent.
Architecture & design decisions
Three urlscan primitives, three VT lookups. urlscan is modeled around its own submit → poll → search loop; VirusTotal around direct object lookups. That mapping keeps each tool a thin, predictable wrapper over one endpoint.
Compact, structured output — not raw API JSON. A single VirusTotal file
report or urlscan result can be hundreds of KB (every AV engine's verdict, the
full DOM, every request/response). Dumping that into a model's context is
wasteful and buries the signal. Every tool projects the response down to the
handful of fields an investigator actually pivots on — detection ratio, threat
label, reputation, ASN/owner, contacted domains/IPs, first/last seen. This is a
deliberate design choice, commented at each summarize_* function in
virustotal.py / urlscan.py.
All tool output is treated as untrusted and defanged. Threat-intel responses
contain attacker-controlled content — a phishing page's title, a malicious
domain, a WHOIS record. MCP tool output is a prompt-injection surface for the
model and a click-hazard for a human reading a terminal. So every indicator
is defanged on the way out (http → hxxp, evil.test → evil[.]test,
1.2.3.4 → 1[.]2[.]3[.]4) at a single central choke point (sanitize.py).
Client-side rate limiting. The VirusTotal public tier allows 4 req/min and
500/day. A per-API sliding-window limiter (ratelimit.py) throttles locally so a
well-behaved server never trips the upstream 429 under normal single-analyst use.
Structured errors, never raw tracebacks. Auth failures, 404s, upstream 429s,
timeouts and local rate-limit hits are all mapped to small categorized error
envelopes (errors.py) so the agent can decide whether to retry, back off, or
ask for a key. They are returned as tool results with isError: true (the
envelope is the JSON text content), so clients can tell a failed lookup from a
successful one.
Tool annotations. Every tool declares MCP annotations: the five lookups are
readOnlyHint: true; scan_url is not, because it submits a new scan. All are
openWorldHint: true since they call third-party services.
Secrets from the environment only. VT_API_KEY and URLSCAN_API_KEY are
read from the environment (or a local, gitignored .env). Nothing is hardcoded.
MCP protocol version
Built on the MCP Python SDK 2.x and targets the 2026-07-28 specification:
stateless requests (no initialize handshake), server/discover, resultType
on every result, and cacheable tools/list results (ttlMs/cacheScope).
The SDK still negotiates earlier revisions (e.g. 2025-11-25) with older clients.
Cross-call state is already explicit: scan_url hands back a uuid that you
pass to get_url_result, which is the pattern the stateless spec recommends.
Transports: stdio vs. Streamable HTTP
stdio (default) — the client spawns the server as a subprocess over stdin/stdout. Best for a single local analyst running Claude Desktop or an IDE MCP client on their own workstation with their own keys.
Streamable HTTP (
--transport http) — the server runs as a long-lived networked process. Best for a shared team deployment: one server holding the org's keys and rate-limit budget, many analysts pointing their clients at it.
Setup
Requires Python 3.10+.
git clone https://github.com/ChristianPresley/threatintel-mcp.git
cd threatintel-mcp
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
cp .env.example .env # then edit .env and add your keysGet API keys:
VirusTotal: https://www.virustotal.com/gui/my-apikey
urlscan.io: https://urlscan.io/user/profile/
Running
# stdio (local analyst) — this is what an MCP client launches for you:
threatintel-mcp --transport stdio
# Streamable HTTP (shared team) — long-lived server on a port:
threatintel-mcp --transport http --host 0.0.0.0 --port 8000 \
--allowed-host 'ti.corp.example:*'The HTTP transport validates the Host and Origin headers to block DNS
rebinding. Loopback names are always allowed; any other hostname clients use to
reach the server must be listed with --allowed-host (repeatable), and browser
origins with --allowed-origin. Requests with any other Host get
421 Misdirected Request.
The HTTP transport has no built-in authentication. Anyone who can reach the port can spend the org's VirusTotal/urlscan quota. For a shared deployment, keep it on a private network and put it behind an authenticating reverse proxy (e.g. OAuth/OIDC or mTLS), and point
--allowed-hostat the proxy's hostname. The server prints a warning when bound to a non-loopback address.
Claude Desktop / MCP client config
Add to claude_desktop_config.json (macOS:
~/Library/Application Support/Claude/, Windows:
%APPDATA%\Claude\):
{
"mcpServers": {
"threatintel": {
"command": "threatintel-mcp",
"args": ["--transport", "stdio"],
"env": {
"VT_API_KEY": "your-virustotal-key",
"URLSCAN_API_KEY": "your-urlscan-key"
}
}
}
}If threatintel-mcp isn't on the client's PATH, use the absolute path to the
venv entry point (e.g. .venv/Scripts/threatintel-mcp.exe on Windows) or invoke
via python -m threatintel_mcp.server.
Worked example: pivoting a suspicious domain
An analyst hands the agent a suspicious link. A natural investigation flow:
scan_url("http://secure-login-microsoft.example/")— submits an unlisted scan (so the adversary isn't tipped off) and returns auuid.get_url_result(uuid)— comes backmalicious: true, brandMicrosoft(credential phish), and a set of contacted domains/IPs includingpage_ip: 203[.]0[.]113[.]9on ASNEVIL-HOST.lookup_domain("secure-login-microsoft.example")— VT shows a creation date three days ago (newly-registered-domain signal) and a couple of engines already flagging it.lookup_ip("203.0.113.9")— the hosting IP has poor reputation and hosts in a country inconsistent with the impersonated brand.search_urlscan("page.ip:203.0.113.9")— passively reveals other phishing pages on the same box, expanding the campaign's footprint.
Five hops, two providers, one MCP server — and every indicator in the transcript is defanged so nothing is accidentally clicked or re-interpreted downstream.
Development
pytest # runs the mocked test suite — no real API calls are made
ruff check . # lintTests mock every HTTP interaction with respx; the suite never touches the
network and needs no real API keys.
License
MIT © 2026 Christian Presley — see LICENSE.
Available Tools
6 toolsget_url_resultGet urlscan.io resultARead-onlyIdempotent
Fetch the results of a urlscan.io scan by its uuid.
Call this after scan_url to retrieve the verdict and the contacted
infrastructure (domains, IPs, ASN, hosting country, page title, TLS issuer).
Scans take roughly 10-30 seconds; if this fails with an error of kind
'pending', wait a few seconds and call again.
Inputs: uuid (required). Returns: object
lookup_domainVirusTotal domain lookupARead-onlyIdempotent
Look up a domain on VirusTotal.
Call this to assess a domain indicator: its detection ratio across URL/domain engines, reputation score, registrar and creation date (a recent creation date is a strong newly-registered-domain signal), and category tags. Useful when pivoting from a URL or email sender to the domain behind it. Returns a compact summary.
Inputs: domain (required). Returns: object
lookup_hashVirusTotal file hash lookupARead-onlyIdempotent
Look up a file hash on VirusTotal.
Call this when you have a file hash (from a sample, an email attachment, an EDR alert, a sandbox report) and want to know whether it's known-malicious: the multi-engine detection ratio, the suggested threat label/family, file type and size, and first/last-seen dates. Accepts MD5, SHA-1 or SHA-256. Returns a compact summary, not the full per-engine report.
Inputs: file_hash (required). Returns: object
lookup_ipVirusTotal IP lookupARead-onlyIdempotent
Look up an IP address on VirusTotal.
Call this to get hosting context and reputation for an IP indicator: the
detection ratio, reputation score, ASN, owning organization, network range
and country. Useful when pivoting from a domain's resolved IP or a
urlscan-reported page_ip to understand who hosts the infrastructure.
Returns a compact summary.
Inputs: ip (required). Returns: object
scan_urlSubmit URL to urlscan.ioA
Submit a URL to urlscan.io for a fresh sandbox scan.
Call this when you have a suspicious or unknown URL and want live analysis:
what it loads, where it redirects, what infrastructure it contacts, and
whether urlscan flags it as malicious. Returns a scan uuid — then poll
get_url_result with that uuid to fetch findings.
OPSEC — visibility defaults to 'unlisted' on purpose. A public scan is indexed and browsable by anyone, including the adversary, who may be watching urlscan for scans of their own infrastructure. Scanning their URL publicly tips them off that they are under investigation and can burn the operation. Use 'public' only when you deliberately want the result shared; use 'private' for the most sensitive cases.
Inputs: url (required), visibility. Returns: object
search_urlscanSearch urlscan.io historyARead-onlyIdempotent
Search urlscan.io's historical scan database (Elasticsearch query syntax).
Call this to find existing scans instead of running a new one — e.g. to see every scan that touched a domain/IP, hunt for a favicon or TLS-cert hash across sites, or discover other pages hosted on the same infrastructure. This is passive: it does not touch the target, so it's safe to use freely during an investigation. Returns a short list of matching scans with their domains, IPs, ASN and result links to pivot from.
Inputs: query (required), size. Returns: object
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 6 tools
Each tool has a clearly distinct purpose: scan_url submits a new scan, get_url_result fetches a specific scan's findings, search_urlscan queries historical scans passively, and lookup_hash/lookup_domain/lookup_ip are separated cleanly by indicator type. The scan/fetch pairing is explicitly documented, so an agent can sequence them without confusion.
All tools follow a verb_noun snake_case pattern (scan_url, get_url_result, search_urlscan, lookup_hash/domain/ip). Minor deviation: the urlscan.io tools encode the source in the noun (urlscan/url_result) while VirusTotal tools use generic verbs, and scan_url vs get_url_result are slightly asymmetric.
Six tools is well-scoped for a threat-intel server, covering the two core workflows (urlscan.io scanning and VirusTotal lookups) without redundancy. Every tool earns its place with no filler.
The surface covers the main IOC pivots (URL submit/fetch, historical scan search, hash/domain/IP reputation), which is solid lifecycle coverage for investigation work. Minor gaps exist: no direct ASN/certificate/WHOIS tool, and no ability to submit samples to VirusTotal, but agents can work around these via the existing tools.
Maintenance
Related MCP Connectors
Security scanner for MCP servers. Detect vulnerabilities, prompt injection, and tool poisoning.
MCP server for Pentest-Tools.com: run scans, manage findings and reports via your preffered LLM.
URLhaus MCP — wraps abuse.ch URLhaus malware URL database (free, no auth)
Official VirusTotal MCP server: threat reports, file and URL submissions, and analysis recovery.
Related MCP Servers
- AlicenseNot gradedqualityAmaintenanceAn MCP server that exposes a 60+ tool security and threat-intel stack to AI agents, enabling secret scanning, Sigma rule generation, ransomware lookup, OSINT, and deep research.1MIT
- FlicenseAqualityDmaintenanceMCP server for security analysis using VirusTotal API, enabling AI assistants to analyze URLs, files, IP addresses, and domains with automatic relationship fetching.81-
- AlicenseCqualityDmaintenanceA Model Context Protocol (MCP) server that enables LLMs to interact with the VirusTotal API for malware analysis, URL scanning, and threat intelligence.81MIT
- AlicenseAqualityCmaintenanceMCP server for urlscan.io that scans URLs, searches historical scan data, and assesses indicators with compact, context-efficient summaries instead of raw API responses.14MIT