Skip to main content
Glama
ChristianPresley

threatintel-mcp

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

scan_url

urlscan.io

Submit a URL for a live sandbox scan (returns a uuid).

get_url_result

urlscan.io

Poll a scan uuid for the verdict + contacted infrastructure.

search_urlscan

urlscan.io

Passively search historical scans (Elasticsearch query syntax).

lookup_hash

VirusTotal v3

Multi-engine verdict + threat label for a file hash (MD5/SHA-1/SHA-256).

lookup_domain

VirusTotal v3

Reputation, registrar, creation date, categories for a domain.

lookup_ip

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 keys

Get API keys:

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-host at 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:

  1. scan_url("http://secure-login-microsoft.example/") — submits an unlisted scan (so the adversary isn't tipped off) and returns a uuid.

  2. get_url_result(uuid) — comes back malicious: true, brand Microsoft (credential phish), and a set of contacted domains/IPs including page_ip: 203[.]0[.]113[.]9 on ASN EVIL-HOST.

  3. 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.

  4. lookup_ip("203.0.113.9") — the hosting IP has poor reputation and hosts in a country inconsistent with the impersonated brand.

  5. 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 .    # lint

Tests 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 tools
get_url_resultGet urlscan.io resultA
Read-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.

ParametersJSON Schema
NameRequiredDescriptionDefault
uuidYesThe scan uuid returned by scan_url.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.4/5.0
Behavior4/5

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.

Conciseness4/5

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.

Completeness5/5

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.

Parameters3/5

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.

Purpose5/5

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.

Usage Guidelines5/5

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 lookupA
Read-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.

ParametersJSON Schema
NameRequiredDescriptionDefault
domainYesA domain name, e.g. example.com.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.1/5.0
Behavior4/5

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.

Conciseness4/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose5/5

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.

Usage Guidelines4/5

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 lookupA
Read-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.

ParametersJSON Schema
NameRequiredDescriptionDefault
file_hashYesA file hash: MD5, SHA-1, or SHA-256.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.3/5.0
Behavior4/5

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.

Conciseness5/5

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.

Completeness5/5

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.

Parameters3/5

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.

Purpose5/5

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.

Usage Guidelines4/5

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 lookupA
Read-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.

ParametersJSON Schema
NameRequiredDescriptionDefault
ipYesAn IPv4 or IPv6 address.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4/5.0
Behavior4/5

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.

Conciseness4/5

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.

Completeness5/5

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.

Parameters3/5

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.

Purpose4/5

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.

Usage Guidelines4/5

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
urlYesThe URL to submit for scanning.
visibilityNoScan visibility: 'unlisted' (default), 'private', or 'public'.unlisted

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.6/5.0
Behavior4/5

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.

Conciseness4/5

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.

Completeness5/5

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.

Parameters4/5

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.

Purpose5/5

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.

Usage Guidelines5/5

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 historyA
Read-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.

ParametersJSON Schema
NameRequiredDescriptionDefault
sizeNoMax results to return (1-100).
queryYesurlscan Elasticsearch query, e.g. 'domain:example.com', 'page.ip:1.2.3.4', 'hash:<sha256>', 'filename:invoice.exe'.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.4/5.0
Behavior4/5

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.

Conciseness4/5

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.

Completeness5/5

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.

Parameters3/5

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.

Purpose5/5

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.

Usage Guidelines5/5

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.

  1. 6 tool updatesv0.2.0
    • First observedget_url_result
    • First observedlookup_domain
    • First observedlookup_hash
    • First observedlookup_ip
    • First observedscan_url
    • First observedsearch_urlscan

TDQS

A4.3/5.0

Scored across 6 tools

Disambiguation5/5

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.

Naming Consistency4/5

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.

Tool Count5/5

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.

Completeness4/5

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

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    A
    maintenance
    An 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.
    1
    MIT
  • F
    license
    A
    quality
    D
    maintenance
    MCP server for security analysis using VirusTotal API, enabling AI assistants to analyze URLs, files, IP addresses, and domains with automatic relationship fetching.
    8
    1
    -
  • A
    license
    A
    quality
    B
    maintenance
    MCP server for urlscan.io that scans URLs, searches historical scan data, and assesses indicators with compact, context-efficient summaries instead of raw API responses.
    14
    MIT