# urldna/mcp [Health: Active]

**Category:** 🔒 Security  
**Repository:** https://github.com/urldna/mcp  
**GitHub Stars:** 7  
**Views:** 5  
**Installs:** 0  
**Upvotes:** 0  
**Directory Page:** https://allmcps.com/mcp/urldna-mcp

## Description
MCP server for automated URL scanning and forensic phishing triage. Captures full DOM snapshots, network requests, and visual screenshots to identify malicious redirects and infrastructure. Supports historical threat hunting using Custom Query Language (CQL) to map actor patterns across millions of recorded scans.

## Tools
Capabilities this server exposes over MCP:

- **new_scan** — Submit a URL to urlDNA for a full scan and wait for the result.

This tool performs a complete website scan: it submits the URL, then polls
the API until the scan finishes (status: DONE) or fails (status: ERROR).
The result is automatically truncated to fit within the model's context window.

The URL is automatically normalized: if no scheme is provided (e.g., "example.com"),
"https://" is prepended automatically.

Polling behavior:
    - Checks every 2 seconds
    - Maximum 30 retries (up to ~60 seconds)
    - Raises an error if the scan does not complete in time

Args:
    url (str): URL to submit for scanning. Can be provided with or without the "https://" prefix.
               Examples: "https://example.com", "example.com", "www.suspicious-site.net"
Returns:
    dict: Truncated scan result JSON from urlDNA, including page metadata,
          technologies, HTTP transactions, threat classification, and more.
Raises:
    RuntimeError: If submission fails, polling fails, or the scan does not complete successfully.
- **get_scan** — Retrieve the full results of a previously submitted urlDNA scan by its ID.

Use this tool when you already have a scan ID (e.g., from new_scan or search results)
and want to fetch the detailed scan report. 

Args:
    scan_id (str): The unique identifier of the scan (e.g., "660d0abc123...").
                   Obtained from new_scan, fast_check, or search results.
Returns:
    dict: Truncated scan result JSON including page metadata, threat classification,
          technologies, certificates, network info, and more.
Raises:
    RuntimeError: If API key retrieval or the HTTP request fails.
- **fast_check** — Quickly check if a URL has already been scanned in the urlDNA database.

This is a lightweight lookup — it does not submit a new scan. Use this
to avoid redundant scans and get instant results if the URL was previously analyzed.

The URL is automatically normalized: if no scheme is provided (e.g., "example.com"),
"https://" is prepended automatically.

Args:
    url (str): URL to check. Can be provided with or without the "https://" prefix.
               Examples: "https://example.com", "example.com", "www.phishing-site.net"
Returns:
    dict: Fast check result from urlDNA, including scan_id if a match is found.
Raises:
    RuntimeError: If the API key retrieval or the HTTP request fails.
- **search** — Search urlDNA scans using CQL (Custom Query Language) syntax.

Returns matching scans sorted by submission date descending (newest first).
Page 1 is available to all users. Pages beyond the first require a PREMIUM subscription.

--- SEARCHABLE FIELDS ---

id               : Scan ID (e.g., 660d0...)
domain           : Scan domain name (e.g., "google.com")
submitted_url    : Submitted URL for the scan (e.g., "https://www.google.com/search")
target_url       : Final redirected URL after following all redirects
redirect_url     : Redirect chain URL
nsfw             : Flag for adult content (true/false)
device           : Device used for the scan (MOBILE/DESKTOP)
user_agent       : Web browser user agent string
http_referer     : URL of the page that made the request
scanned_from     : Country from which the scan was initiated
submitter_tag    : User-defined submitter tags
origin           : Source of the scan (USER/API)
protocol         : Protocol used (HTTP/HTTPS)
title            : Page title
text             : Full page text (PREMIUM users only)
ip               : IP address used to access the website
org              : Organization associated with the IP
isp              : Internet Service Provider for the IP
asn              : Autonomous System Number associated with the IP
city             : City associated with the IP
country_code     : Country code associated with the IP (e.g., "IT", "US")
http_transaction : HTTP transaction URL
outgoing_link    : Page outgoing link
favicon          : Perceptual hash of the website's favicon image
screenshot       : Hash of the website's screenshot
registrar        : Whois registrar attribute
serial_number    : Certificate serial number (for SSL certificates)
issuer           : Certificate issuer (for SSL certificates)
subject          : Certificate subject (for SSL certificates)
malicious        : Flag for malicious website (true/false)
threat           : Detected threat classification
technology       : Technologies used by the website
cookie_name      : Name of a specific cookie found on the website
cookie_value     : Value of a specific cookie found on the website
category         : Page category (e.g., "Security") (PREMIUM users only)
language         : Detected page language (e.g., "English") (PREMIUM users only)
topic            : Page topics (e.g., "phishing detection") (PREMIUM users only)

--- OPERATORS ---

=      Exact match            → domain = google.com
!=     Exclude exact value    → domain != facebook.com
LIKE   Partial/wildcard match → title LIKE Login
!LIKE  Exclude pattern        → domain !LIKE amazon

Combine multiple filters using AND (both must be true) or OR (at least one must be true):
Combine multiple filters using AND or OR:
    domain = www.google.com AND title LIKE search
    domain = google.com OR domain = youtube.com

--- EXAMPLES ---

CORRECT (Partial search):
    query="domain LIKE google" 
CORRECT (Exact search):
    query="domain = www.google.com"
WRONG (Will fail to find subdomains):
    query="domain = google"

Find scans from mobile devices in either Italy or Spain:
    query="device = MOBILE AND country_code = IT OR country_code = ES"

Find malicious scans using either WordPress or Shopify:
    query="malicious = true AND technology LIKE wordpress OR technology LIKE shopify"

Find scans matching a specific favicon hash:
    query="favicon LIKE d417e43"

Find scans with a specific domain and page title pattern:
    query="domain = www.google.com AND title LIKE search"

Find scans for either of two domains:
    query="domain = google.com OR domain = youtube.com"

Find non-NSFW scans submitted via API from Germany:
    query="nsfw = false AND origin = API AND country_code = DE"

Get second page of results (PREMIUM only):
    search(query="malicious = true", page=2)

Args:
    query (str): Search expression in urlDNA CQL syntax.
                 Combine multiple conditions with AND or OR.
                 Combine multiple conditions with AND or OR.
    page (int, optional): Page number for pagination (1-indexed). Default is 1.
                          Pages beyond page 1 require a PREMIUM subscription.
Returns:
    list[dict]: Array of Scan objects matching the query, sorted by submission date
                descending. Each scan includes submitted_url, domain, status, device,
                malicious flag, country, and more.
Raises:
    RuntimeError: If the search request fails or the API key is missing.
- **list_queries** — List all saved queries for the authenticated user.

Saved queries are reusable filter combinations that can be executed against the
urlDNA scan database. Each query has a name, a unique ID, and one or more filter
conditions that are combined with logical AND.

Use this to discover existing queries before creating new ones or to retrieve
query IDs needed for get_query, query_scans, update_query, or delete_query.

Returns:
    list[dict]: Array of Query objects, each containing:
        - id (str): Unique query identifier.
        - name (str): User-defined name.
        - query_filters (list): Array of filter conditions (attribute, operator, value).
Raises:
    RuntimeError: If the API key retrieval or HTTP request fails.
- **get_query** — Retrieve the full details of a specific saved query by its ID.

Returns the query name and all filter conditions configured for it.
Use list_queries first if you don't know the query ID.

Args:
    query_id (str): Unique identifier of the saved query (e.g., "abc123...").
Returns:
    dict: A Query object containing:
        - id (str): Unique query identifier.
        - name (str): User-defined name.
        - query_filters (list): Array of filter conditions, each with:
            - attribute (str): The scan attribute to filter on.
            - operator (str): Comparison operator (=, !=, LIKE, !LIKE).
            - value (str): The value to match against.
            - logical_operator (str): Logical operator to combine with the next filter (AND, OR).
Raises:
    RuntimeError: If the query ID is not found or the request fails.
- **query_scans** — Execute a saved query and return all matching scans.

Runs all filter conditions stored in the query against the urlDNA database
and returns matching scans sorted by submission date (newest first).
Pages beyond the first require a PREMIUM subscription.

Args:
    query_id (str): Unique identifier of the saved query to execute.
    page (int, optional): Page number for pagination (1-indexed). Default is 1.
                          Pages > 1 require PREMIUM subscription.
Returns:
    list[dict]: Array of Scan objects matching the query filters, sorted by
                submission date descending. Each scan includes metadata such as
                submitted_url, domain, status, device, malicious flag, etc.
Raises:
    RuntimeError: If the query ID is not found or the request fails.
- **create_query** — Create a new saved query with one or more filter conditions.

Saved queries allow reusable, complex searches across the urlDNA database.
Requires a PREMIUM subscription.

--- QUERY FILTER STRUCTURE ---
Each filter in query_filters must be a dict. 
- The FIRST filter (index 0) requires: attribute, operator, value.
- SUBSEQUENT filters (index 1+) require: attribute, operator, value, AND logical_operator.

The 'logical_operator' defines how the current filter connects to the 
filters defined before it.

--- VALID ATTRIBUTES ---
id, domain, ip, submitted_url, target_url, scanned_from, user_agent, nsfw,
device, origin, asn, org, protocol, title, city, country_code, isp, favicon,
screenshot, issuer, serial_number, subject, malicious, technology, cookie_name,
http_referer, cookie_value, http_transaction, outgoing_link, submitter_tag,
registrar, category, topic, language, text, redirect_url, threat

--- VALID OPERATORS ---
=      Exact match             (e.g., malicious = true)
!=     Exclude exact value     (e.g., device != MOBILE)
LIKE   Partial/wildcard match  (e.g., technology LIKE wordpress)
!LIKE  Exclude pattern         (e.g., domain !LIKE amazon)

--- EXAMPLES ---

1. Find malicious scans where the title is 'login' OR 'register':
    query_filters = [
        {
            "attribute": "malicious",
            "operator": "=",
            "value": "true"
        },
        {
            "attribute": "title",
            "operator": "LIKE",
            "value": "login",
            "logical_operator": "AND"
        },
        {
            "attribute": "title",
            "operator": "LIKE",
            "value": "register",
            "logical_operator": "OR"
        }
    ]

2. Find scans using WordPress, but NOT from facebook.com:
    query_filters = [
        {
            "attribute": "technology", 
            "operator": "LIKE", 
            "value": "wordpress"
        },
        {
            "attribute": "domain",     
            "operator": "!=",   
            "value": "facebook.com", 
            "logical_operator": "AND"
        }
    ]

Args:
    name (str): Descriptive name for the saved query (e.g., "Login Phish Search").
    query_filters (list[dict]): Array of filter conditions. 
        - attribute (str): Scan field to filter on.
        - operator  (str): One of =, !=, LIKE, !LIKE.
        - value     (str): Value to match against the attribute.
        - logical_operator (str, optional): Required for all filters except the first. 
        Must be 'AND' or 'OR'.
Returns:
    list[dict]: Array of all user Query objects after creation.
Raises:
    ValueError: If any filter is missing required fields, has invalid values, 
                or if a logical_operator is missing on chained filters.
    RuntimeError: If the API key retrieval or HTTP request fails.
- **update_query** — Update an existing saved query's name and/or filter conditions.

Replaces ALL previous filter conditions with the new provided list.
Partial updates are not supported — always provide the full desired filter set.
Requires a PREMIUM subscription.

--- QUERY FILTER STRUCTURE ---
Each filter in query_filters must be a dict. 
- The FIRST filter (index 0) requires: attribute, operator, value.
- SUBSEQUENT filters (index 1+) require: attribute, operator, value, AND logical_operator.

The 'logical_operator' defines how the current filter connects to the 
filters defined before it.

--- VALID ATTRIBUTES ---
id, domain, ip, submitted_url, target_url, scanned_from, user_agent, nsfw,
device, origin, asn, org, protocol, title, city, country_code, isp, favicon,
screenshot, issuer, serial_number, subject, malicious, technology, cookie_name,
http_referer, cookie_value, http_transaction, outgoing_link, submitter_tag,
registrar, category, topic, language, text, redirect_url, threat

--- VALID OPERATORS ---
=      Exact match             (e.g., malicious = true)
!=     Exclude exact value     (e.g., device != MOBILE)
LIKE   Partial/wildcard match  (e.g., technology LIKE wordpress)
!LIKE  Exclude pattern         (e.g., domain !LIKE amazon)

--- EXAMPLE: UPDATING TO MULTIPLE FILTERS ---
To update a query to find (Malicious AND Title LIKE 'login') OR (Title LIKE 'register'):

query_filters = [
    {
        "attribute": "malicious",
        "operator": "=",
        "value": "true"
    },
    {
        "attribute": "title",
        "operator": "LIKE",
        "value": "login",
        "logical_operator": "AND"
    },
    {
        "attribute": "title",
        "operator": "LIKE",
        "value": "register",
        "logical_operator": "OR"
    }
]

Args:
    query_id (str): Unique identifier of the query to update.
    name (str): New name for the query.
    query_filters (list[dict]): Full replacement filter list. Each dict requires:
        - attribute (str): Scan field to filter on.
        - operator  (str): One of =, !=, LIKE, !LIKE.
        - value     (str): Value to match against the attribute.
        - logical_operator (str, optional): Required for all filters except the first. 
          Must be 'AND' or 'OR'.
Returns:
    dict: The updated Query object.
Raises:
    ValueError: If any filter is missing required fields, has invalid values, 
                or if a logical_operator is missing on chained filters.
    RuntimeError: If the query ID is not found or the request fails.
- **delete_query** — Permanently delete a saved query by its ID.

This operation is irreversible — the query and all its filter conditions
are permanently removed. The scans matched by the query are not affected.

Use list_queries to confirm the query ID before deleting.

Args:
    query_id (str): Unique identifier of the query to delete.
Returns:
    dict: Confirmation response from the API.
Raises:
    RuntimeError: If the query ID is not found or the request fails.
- **list_brands** — List all brands available to the authenticated user for threat monitoring.

Brands represent organizations or entities whose domains and assets are tracked
in the urlDNA database. Use brands to monitor phishing attempts, brand impersonation,
and suspicious lookalike domains. Requires PREMIUM subscription.

--- FILTER OPTIONS ---
ALL         : All brands accessible to the user (default if omitted).
FREE        : Public brands available to all users.
PREMIUM     : Premium-tier brands with advanced monitoring.
USER_BRANDS : Brands created by the authenticated user.

--- EXAMPLES ---
List all brands:
    list_brands()

Search for brands containing "google":
    list_brands(query="google")

List only user-created brands:
    list_brands(filter="USER_BRANDS")

Search public brands with pagination:
    list_brands(filter="FREE", query="microsoft", page=2)

Args:
    filter (str, optional): Visibility filter for brands.
        One of: ALL, FREE, PREMIUM, USER_BRANDS. Default: returns all accessible brands.
    query (str, optional): Search term for filtering brands by name (case-insensitive partial match).
                           Example: "google" will match "Google LLC", "Google Cloud", etc.
    page (int, optional): Page number for pagination (1-indexed). Default is 1.
                          Pages > 1 require PREMIUM subscription.
Returns:
    list[dict]: Array of Brand objects, each containing:
        - id (str): Unique brand identifier.
        - name (str): Brand name.
        - private_brand (bool): True if only visible to the creating user.
Raises:
    ValueError: If an invalid filter value is provided.
    RuntimeError: If the API key retrieval or HTTP request fails.
- **get_brand** — Retrieve the full details of a specific brand by its ID.

Returns complete brand metadata including configuration and visibility settings.
Use list_brands first if you need to look up a brand ID by name.
Requires PREMIUM subscription.

Args:
    brand_id (str): Unique identifier of the brand (e.g., "abc123...").
                    Obtain from list_brands.
Returns:
    dict: A Brand object containing:
        - id (str): Unique brand identifier.
        - name (str): Brand name.
        - private_brand (bool): True if only visible to the creating user.
Raises:
    RuntimeError: If the brand ID is not found or the request fails.
- **brand_scans** — Retrieve all scans associated with a specific brand.

Returns scans linked to the brand, sorted by submission date (newest first).
Optionally filter results further using CQL (Custom Query Language) syntax
for more targeted threat analysis. Requires PREMIUM subscription.

--- CQL FILTER SYNTAX (optional query parameter) ---
Combine scan attributes using AND or OR:
    malicious = true AND device = MOBILE
    domain = paypal.com OR domain = paypal-login-example.com
    country_code = IT AND technology LIKE phishing

Supported operators: =, !=, LIKE, !LIKE

Searchable attributes include:
    domain, ip, submitted_url, target_url, malicious, device, country_code,
    technology, title, user_agent, scanned_from, and many more.
    See the search tool docstring for the full attribute list.

--- EXAMPLES ---
Get all scans for a brand:
    brand_scans(brand_id="abc123")

Get only malicious scans for a brand:
    brand_scans(brand_id="abc123", query="malicious = true")

Get mobile scans from Italy for a brand:
    brand_scans(brand_id="abc123", query="device = MOBILE AND country_code = IT")

Get scans for either of two domains within a brand:
    brand_scans(brand_id="abc123", query="domain = paypal.com OR domain = paypal-login-example.com")

Get second page of results:
    brand_scans(brand_id="abc123", page=2)

Args:
    brand_id (str): Unique identifier of the brand to retrieve scans for.
                    Obtain from list_brands or get_brand.
    query (str, optional): Optional CQL expression for additional filtering.
                           See POST /v1/search documentation for full CQL syntax.
    page (int, optional): Page number for pagination (1-indexed). Default is 1.
                          Pages > 1 require PREMIUM subscription.
Returns:
    list[dict]: Array of Scan objects associated with the brand, sorted by
                submission date descending. Each scan includes submitted_url,
                domain, device, malicious flag, status, country, and more.
Raises:
    RuntimeError: If the brand ID is not found or the request fails.
- **search_docs** — Search the official urlDNA documentation for integrations, API usage, and guides.

Args:
    query (str): The search term (e.g., 'Slack integration', 'API rate limits').

## Claude Desktop Quick Installation
Remote MCP endpoint (confidence: high). Install path detected from listing signals. Add as a URL/SSE server in your client:

```json
"mcpServers": {
  "mcp": {
    "url": "https://mcp.urldna.io/"
  }
}
```

## Documentation

## What urldna/mcp MCP server does

The urldna/mcp MCP server exposes urlDNA threat intelligence capabilities through the Model Context Protocol. Agents can check whether a URL has already been scanned, submit a URL for a full scan, and retrieve scan results by ID. Full scan investigations can include DOM snapshots, network requests, and visual screenshots for examining redirects and suspicious infrastructure.

The server also supports historical searches across recorded scans with urlDNA’s Custom Query Language (CQL). Search conditions can cover domains, IP addresses, technologies, malicious status, and other scan attributes. CQL expressions support `AND` and `OR` operators, with pagination available for search results; pages after the first require PREMIUM access.

## How it works

The urldna/mcp MCP server offers the same toolset through two transports. Local desktop clients can launch it over stdio. Hosted deployments can expose it through streamable HTTP, including the published endpoint at `https://mcp.urldna.io/`.

The server passes requests to the urlDNA platform API and requires a urlDNA API key for authenticated access. Compatible MCP clients can discover or call tools for scanning, search, saved queries, brand monitoring, and API documentation. Claude Desktop can use the local transport or connect to the hosted endpoint.

## Setup and configuration

For a local deployment, clone the repository, install its Python dependencies with `uv sync`, and start stdio mode with:

```bash
uv run python urldna_mcp/run.py
```

Streamable HTTP mode starts with:

```bash
uv run python urldna_mcp/server.py
```

The HTTP process reads `PORT`, defaulting to `8080`, and `MCP_PATH`, defaulting to `/`. These settings control the listening port and public MCP endpoint path. Client configuration supplies the urlDNA credential as the `x-api-key` header. Claude Desktop configuration uses the `uv` command with the repository directory and `urldna_mcp/run.py` as arguments.

For a hosted connection, configure the client with `https://mcp.urldna.io/` and provide the API key in the request headers. The repository’s examples show this pattern for OpenAI and Gemini clients.

## Tools and capabilities

Available tool groups include:

- `fast_check` for an immediate SAFE, MALICIOUS, or UNRATED result when a URL has been scanned.
- `new_scan` for submitting a URL and waiting approximately 30–60 seconds for a full result.
- `get_scan` for retrieving a complete scan by ID.
- `search` for CQL searches over recorded scans.
- `list_queries`, `get_query`, `create_query`, `update_query`, `delete_query`, and `query_scans` for saved-query management.
- `list_brands`, `get_brand`, and `brand_scans` for brand data and associated scans.
- `search_docs` for retrieving the urlDNA OpenAPI specification and documentation.

## Limitations and notes

Some capabilities are access-controlled. Search pagination beyond the first page requires PREMIUM access, and the README labels saved-query and brand-monitoring tools as PREMIUM. Running the local server also requires the repository, `uv`, and a urlDNA API key. The hosted endpoint avoids local process management but still requires the urlDNA credential.

The server returns urlDNA platform data; it does not replace analyst review of suspicious pages or infrastructure. Scan completion for `new_scan` is asynchronous from the user’s perspective and may take approximately 30–60 seconds.

_Full upstream README: https://allmcps.com/mcp/urldna-mcp/readme_

