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-mcplookup 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.
| Name | Required | Description | Default |
|---|---|---|---|
| uuid | Yes | The scan uuid returned by scan_url. |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly, idempotent, non-destructive and openWorld, so safety is covered. The description adds genuinely new behavioral context beyond those: the scan latency window, the pending-error failure mode, and the retry instruction. It loses a point only because the enumeration of return fields partially duplicates the output schema.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the action and key, then two tight sentences of context. The parenthetical list of returned infrastructure is slightly redundant given an output schema exists, but nothing is bloated.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With an output schema, rich annotations, and a single documented parameter, the description supplies the only missing piece an agent needs: the polling/retry behavior and timing expectation. Nothing required to call it correctly is absent.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
One parameter with 100% schema description coverage, so the schema already explains that uuid is the scan uuid returned by scan_url. The description adds no format or syntax detail beyond that, which is the expected baseline when the schema does the work.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (fetch) and resource (urlscan.io scan results) keyed by uuid. It also names the sibling scan_url as the producer of that uuid, so the agent can distinguish it from search_urlscan or the lookup_* tools.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly says when to call it (after scan_url), what to expect in timing (10-30 seconds), and what to do on failure ('pending' error → wait and retry). This is exactly the when/when-not guidance an agent needs for an async polling pattern.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
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.
| Name | Required | Description | Default |
|---|---|---|---|
| domain | Yes | A domain name, e.g. example.com. |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly, idempotent, non-destructive, open-world behavior, so the safety profile is covered. The description adds genuine context beyond that: which fields are returned (detection ratio, reputation, registrar, creation date, category tags) and the analytic insight that a recent creation date is a newly-registered-domain signal. It does not discuss rate limits or quota behavior, which is the main remaining gap.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the core action, then the indicator-assessment context, then the return summary. Every sentence is relevant, though the enumeration of returned fields is slightly long and could be trimmed.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given a 1-param schema at full coverage, rich annotations, and an existing output schema, the description is close to complete: it says what is looked up, why, and roughly what comes back. Only missing element is explicit routing against the sibling lookup/scan tools.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
There is a single parameter with 100% schema description coverage ('A domain name, e.g. example.com'), so the schema already carries the parameter meaning. The description adds no format or syntax detail beyond it, which matches the baseline 3 for high-coverage schemas.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb ('Look up') and resource ('a domain on VirusTotal') and is clearly differentiated from siblings like lookup_ip, lookup_hash, and scan_url. The second sentence enumerates exactly what is being assessed, so an agent can tell which indicator type this tool covers without opening the schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly frames when to call it: 'to assess a *domain* indicator' and 'when pivoting from a URL or email sender to the domain behind it.' That gives a clear usage context, but it never names the alternative siblings (e.g., scan_url for raw URLs) or states when not to use it.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
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.
| Name | Required | Description | Default |
|---|---|---|---|
| file_hash | Yes | A file hash: MD5, SHA-1, or SHA-256. |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly/idempotent/openWorld/non-destructive, so the description only needs to add beyond that. It does: accepted hash algorithms (MD5, SHA-1, SHA-256) and the crucial disclosure that the response is a compact summary rather than the full per-engine report. It omits API-key/rate-limit behavior, which keeps it from a 5.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the one-line purpose, then a tightly packed usage sentence listing trigger sources and returned fields. Every clause adds information; nothing is filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
One required parameter, full schema coverage, and an output schema that carries return structure. The description supplies exactly the gaps an agent needs: when to reach for it and that the result is a summarized detection ratio rather than the raw engine breakdown.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% and the single file_hash parameter is fully documented in the schema; the description only restates the accepted hash formats. With the schema doing the heavy lifting, baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (look up) and resource (a file hash on VirusTotal), and the return content makes the scope unambiguous. The resource type cleanly separates it from siblings lookup_domain, lookup_ip, and scan_url, which target different observable types.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly says when to call it and enumerates concrete sources of a hash (sample, email attachment, EDR alert, sandbox report) plus the goal (known-malicious?). It does not name alternative sibling tools or state when NOT to use it, so it falls short of a 5.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
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.
| Name | Required | Description | Default |
|---|---|---|---|
| ip | Yes | An IPv4 or IPv6 address. |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover the safety profile (readOnly, idempotent, openWorld, non-destructive), so the description only needs to add context beyond that -- which it does, by disclosing the shape of the response ("compact summary") and the exact fields returned. It does not mention rate limits or quota behavior, keeping it out of the 5 range.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The purpose is front-loaded, followed by when-to-use and a return summary; almost every sentence earns its place. A small formatting blemish (double-backtick ``page_ip``) and slight redundancy in restating the return twice keep it from a 5.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a single-parameter read tool with an output schema and rich annotations, the description covers purpose, selection context, and response shape. An agent has everything needed to call it correctly without further inference.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100% and the single `ip` parameter is documented as "An IPv4 or IPv6 address" in the schema itself. The description adds nothing about the parameter beyond the schema, so the adequate-but-unremarkable baseline of 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb and resource ("Look up an IP address on VirusTotal") and enumerates exactly what is returned (detection ratio, reputation, ASN, org, network range, country). It never names a specific sibling, but the IP-vs-domain-vs-hash resource split makes it distinguishable from lookup_domain and lookup_hash.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It gives clear positive triggers: use it for an *IP indicator* and when pivoting from a domain's resolved IP or a urlscan-reported ``page_ip``. There is no explicit when-not guidance or named alternative, so it stops short of the top tier.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
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.
| Name | Required | Description | Default |
|---|---|---|---|
| url | Yes | The URL to submit for scanning. | |
| visibility | No | Scan visibility: 'unlisted' (default), 'private', or 'public'. | unlisted |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already mark this as a non-read-only, open-world, non-idempotent write, but the description adds substantial context the annotations cannot: default visibility is 'unlisted', public scans are indexed and adversary-visible, and public scanning can burn an investigation. That OPSEC disclosure is genuinely valuable. It stops short of covering rate limits, quota, or scan latency, so not a full 5.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the action and the follow-up tool, then the OPSEC block, which is longer than strictly necessary but every sentence carries decision-relevant content about visibility choice. No filler or repetition of the schema.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists so return values need not be fully documented, yet the description still tells the agent the key return artifact (uuid) and what to do with it (poll get_url_result). For a 2-parameter submission tool this covers everything needed to invoke it correctly and safely.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline would be 3, but the description adds real meaning beyond the schema's terse enum gloss by explaining the operational consequence of choosing 'public' versus 'unlisted' versus 'private'. It still leaves 'private' vs 'unlisted' slightly under-differentiated, so it is not a clean 5.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb+resource ('Submit a URL to urlscan.io for a fresh sandbox scan') and immediately distinguishes itself from the sibling get_url_result by naming it as the polling follow-up. An agent can tell this is the submission step, not the retrieval step, without opening either schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives an explicit trigger condition ('when you have a suspicious or unknown URL') plus what the tool is for (live analysis: loads, redirects, infrastructure, maliciousness flag). It routes the agent onward to get_url_result with the returned uuid, so the workflow ordering is unambiguous.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
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.
| Name | Required | Description | Default |
|---|---|---|---|
| size | No | Max results to return (1-100). | |
| query | Yes | urlscan Elasticsearch query, e.g. 'domain:example.com', 'page.ip:1.2.3.4', 'hash:<sha256>', 'filename:invoice.exe'. |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint and destructiveHint=false, so the safety profile is known. The description adds genuine operational context beyond that: it is passive, does not touch the target, and is safe to reuse during an investigation, plus it sketches the shape of the results (domains, IPs, ASN, links).
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The purpose and scope are front-loaded in the first sentence, and the routing advice follows immediately. Four sentences carry some phrasing that could be tightened, but each sentence contributes distinct information (purpose, alternatives, safety, returns).
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With an output schema present and full annotation coverage, the description needn't explain return values in detail, yet it still summarizes them. Purpose, alternative routing, safety, and query framing are all present, leaving nothing an agent needs to call this correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so both parameters are fully documented in the schema, including query syntax examples and the size range. The description reinforces the Elasticsearch query framing but adds no per-parameter detail beyond what the schema already provides, so the baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (Search) and resource (urlscan.io's historical scan database) and immediately frames scope with the Elasticsearch query syntax. It is clearly distinguishable from scan_url by emphasizing *existing* scans, so an agent can separate it from siblings without opening a schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly routes the agent: 'Call this to find *existing* scans instead of running a new one,' naming the alternative behavior (i.e. scan_url) and the condition that selects it. It reinforces with three concrete use cases (domain/IP history, favicon/TLS-cert hunting, shared-infrastructure discovery).
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
6 tool updates
v0.2.0- First observed
get_url_result - First observed
lookup_domain - First observed
lookup_hash - First observed
lookup_ip - First observed
scan_url - First observed
search_urlscan
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)
MCP server for secureFlows: token-free URL builders and integration-linting tools for AI agents.
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
- AlicenseAqualityBmaintenanceMCP 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