# Connect Codex to VirusTotal MCP

Query limits: 60 admitted queries per fixed 60-second window and 1000 per UTC day. Agent Tokens share the agent's allowance across REST and MCP; OAuth connections share the account's allowance. Unknown reports and upstream failures after admission count. Every repeated lookup counts again, including cached reports and hashes with no report. On 429, honor Retry-After and reuse your credential. File submissions first check whether the hash exists in VirusTotal. Only confirmed unknown files are uploaded, without using query quota, even when the query allowance is exhausted. A separate file contribution limit permits 20 admitted upload attempts per fixed 60-second window and 500 per UTC day, per Agent Token identity or OAuth account across connections. Failed or uncertain admitted attempts still count; known files and receipt recovery do not. Creating multiple identities or accounts to evade limits is not permitted. If the file is known, returning its existing report costs one query and no file is uploaded. Explicit hash lookups and analysis-result reads still count. The access check costs no query quota and does not report remaining quota.

Standard VirusTotal submissions share content with the security community and partners; they are not confidential. Upload files or attachments, submit URLs, or request domain/IP reanalysis only when the user explicitly requests that action. A missing report, an unfamiliar file or a suspicious indicator is not permission to submit it. Explain the sharing and ask before submitting the user's own documents, internal code, credentials or personal data. Submission tools add no per-call confirmation; client permissions still apply.

Use VirusTotal evidence before running a downloaded file, installing unfamiliar code, or trusting a link. Look up hashes, URLs, domains and IPs; analyze files and URLs, request domain/IP reanalysis and recover results. A missing report or no detections does not establish safety.

Transport: **http**. MCP endpoint: `https://ai.virustotal.com/mcp`. This recipe uses a VTAI Agent Token. Access is free within the published quotas; no VirusTotal API key is needed. Keep your existing model-provider login; that account and its charges are separate.

For local file access, use the [stdio instructions](https://ai.virustotal.com/connect/mcp?client=codex&transport=stdio&auth=token&format=markdown). For a connection without a local server, use [HTTP instructions](https://ai.virustotal.com/connect/mcp?client=codex&transport=http&auth=token&format=markdown).

Client setup: [Antigravity CLI (agy)](https://ai.virustotal.com/connect/mcp?client=agy&format=markdown) · [Claude Code](https://ai.virustotal.com/connect/mcp?client=claude&format=markdown) · [Codex](https://ai.virustotal.com/connect/mcp?client=codex&format=markdown) · [Cursor](https://ai.virustotal.com/connect/mcp?client=cursor&format=markdown) · [VS Code](https://ai.virustotal.com/connect/mcp?client=vscode&format=markdown) · [GitHub Copilot CLI](https://ai.virustotal.com/connect/mcp?client=copilot&format=markdown) · [Devin Local / CLI](https://ai.virustotal.com/connect/mcp?client=devin&format=markdown) · [Cascade / Windsurf](https://ai.virustotal.com/connect/mcp?client=cascade&format=markdown)

Agent Token: stdio and HTTP report and analysis reads verified with Codex CLI 0.153.4 on 7 September 2026. These checks do not cover every current tool; browser OAuth was tested separately.

## Recommended: set up once

Install your client and Python 3.11+, then choose the command for your operating system. macOS/Linux uses python3 and curl; Windows uses PowerShell and the Python launcher (py). Codex HTTP setup requires Codex 0.154.0 or newer. The installer reuses your protected VTAI credential or creates one if none exists, then configures this client at user scope. New handles and activity totals may appear on the public leaderboard. It keeps other client settings and does not put credentials in shell arguments or URLs. Run it only when setup is authorized; do not also run the manual registration below.

### macOS / Linux

```bash
(
  set -eu
  vt_setup_dir=$(mktemp -d)
  trap 'rm -rf "$vt_setup_dir"' EXIT
  curl --fail --silent --show-error --proto '=https' --max-time 30 \
    https://ai.virustotal.com/setup.py -o "$vt_setup_dir/setup.py"
  python3 "$vt_setup_dir/setup.py" --client codex --transport http
)
```

### Windows PowerShell

```powershell
& {
  $ErrorActionPreference = 'Stop'
  $vtSetupFile = [IO.Path]::GetTempFileName()
  try {
    Invoke-WebRequest -UseBasicParsing -MaximumRedirection 0 -TimeoutSec 30 `
      'https://ai.virustotal.com/setup.py' -OutFile $vtSetupFile
    py -3 $vtSetupFile --client codex --transport http
    if ($LASTEXITCODE -ne 0) { throw 'VirusTotal setup did not complete.' }
  } finally {
    Remove-Item -LiteralPath $vtSetupFile -Force
  }
}
```

[Inspect or download the installer](https://ai.virustotal.com/setup.py). To import an existing token stored elsewhere, add `--token-file /path/to/token` to the Python command instead of creating access. Afterwards open your client normally, accept its trust prompt if shown, and make the first `get_domain_report` call below. No per-session launch wrapper is needed. Rerunning setup should reuse this access; an uncertain registration is not retried automatically.

### Check an existing installation

Use `--check --json` to inspect user configuration and validate an existing managed Agent Token. The check does not register, change client settings or spend report quota. It reports `credential_active` only after a successful access check; this does not verify remaining quota, the upstream service or an actual tool call. OAuth and custom configurations can require native-client verification. JSON output contains `state`, `client`, `transport`, `next_action`, `error` and `maintenance`, never credentials. Maintenance reports a configured package version where recognizable; installed package version remains unknown, and runtime and tool verification remain false. An old downloaded installer only knows its own target version, so these commands download the current installer before checking. Exit 0 means the stated check succeeded; exit 1 means action is needed and exit 2 means verification is incomplete. Add `--json` to normal setup for structured installation results.

macOS / Linux:

```bash
(
  set -eu
  vt_setup_dir=$(mktemp -d)
  trap 'rm -rf "$vt_setup_dir"' EXIT
  curl --fail --silent --show-error --proto '=https' --max-time 30 \
    https://ai.virustotal.com/setup.py -o "$vt_setup_dir/setup.py"
  python3 "$vt_setup_dir/setup.py" --client codex --check --json
)
```

Windows PowerShell:

```powershell
& {
  $ErrorActionPreference = 'Stop'
  $vtSetupFile = [IO.Path]::GetTempFileName()
  try {
    Invoke-WebRequest -UseBasicParsing -MaximumRedirection 0 -TimeoutSec 30 `
      'https://ai.virustotal.com/setup.py' -OutFile $vtSetupFile
    py -3 $vtSetupFile --client codex --check --json
    if ($LASTEXITCODE -eq 1) { Write-Warning 'Setup needs action; read the result above.' }
    elseif ($LASTEXITCODE -eq 2) { Write-Warning 'Verify the connection in your client; read the result above.' }
    elseif ($LASTEXITCODE -ne 0) { throw 'VirusTotal setup did not complete.' }
  } finally {
    Remove-Item -LiteralPath $vtSetupFile -Force
  }
}
```

### Update an existing installation

Use `--update --json` after reviewing the check. It detects the saved transport, updates only a recognized uv package pin or installer-managed credential helper, and preserves credentials and other client settings. It never registers, imports a token or downgrades a newer package. Custom commands, authentication and unsupported TOML layouts require a manual update using their original configuration; do not replace them with a fresh registration. For a local server, uv downloads the pinned package on the next connection. For remote HTTP, VirusTotal updates the server independently; native OAuth remains in the client. Restart or reconnect, run the check again, then make the first tool call. Updating configuration alone does not test a connection.

macOS / Linux:

```bash
(
  set -eu
  vt_setup_dir=$(mktemp -d)
  trap 'rm -rf "$vt_setup_dir"' EXIT
  curl --fail --silent --show-error --proto '=https' --max-time 30 \
    https://ai.virustotal.com/setup.py -o "$vt_setup_dir/setup.py"
  python3 "$vt_setup_dir/setup.py" --client codex --update --json
)
```

Windows PowerShell:

```powershell
& {
  $ErrorActionPreference = 'Stop'
  $vtSetupFile = [IO.Path]::GetTempFileName()
  try {
    Invoke-WebRequest -UseBasicParsing -MaximumRedirection 0 -TimeoutSec 30 `
      'https://ai.virustotal.com/setup.py' -OutFile $vtSetupFile
    py -3 $vtSetupFile --client codex --update --json
    if ($LASTEXITCODE -ne 0) { throw 'VirusTotal setup did not complete.' }
  } finally {
    Remove-Item -LiteralPath $vtSetupFile -Force
  }
}
```

## Advanced: manual setup

The steps below are an alternative to the installer for existing environment-based or custom setups. Keep a single configuration for this server; project settings may override user settings.

## 1. Reuse or create access

Reuse `~/.config/vt-mcp/token` if configured. Changing clients does not require registration. Keep credentials out of prompts, tool arguments, URLs and printed command output. An authorized setup process may store the token directly without exposing it to model context. This Agent Token path needs no browser sign-in. Do not register again after a 401 or 429; check credential mapping or respect Retry-After instead.

If no credential exists and setup is authorized, this POSIX Python 3 command registers once and saves it with owner-only permissions. The handle and activity totals may appear on the public leaderboard. It sends one registration request, with no automatic retry on an uncertain result. Windows needs equivalent user-only file permissions.

```bash
python3 - <<'PY'
import json
import os
import re
from pathlib import Path
from urllib.request import HTTPRedirectHandler, Request, build_opener

class NoRedirect(HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None

path = Path.home() / ".config" / "vt-mcp" / "token"
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
path.parent.chmod(0o700)
if path.exists() or path.is_symlink():
    raise SystemExit("Reuse the existing credential; registration was not sent.")
request = Request(
    "https://ai.virustotal.com/api/v3/agents/register",
    data=json.dumps({"agent_family": "vt-mcp-codex", "agent_version": "0.9.9"}).encode(),
    headers={"Content-Type": "application/json"}, method="POST",
)
try:
    with build_opener(NoRedirect).open(request, timeout=20) as response:
        if response.status != 200:
            raise ValueError("Unexpected status")
        raw = response.read(8193)
        if len(raw) > 8192:
            raise ValueError("Response limit")
        token = json.loads(raw)["agent_token"]
        if not isinstance(token, str) or not re.fullmatch(r"vtai_[A-Za-z0-9_-]{1,507}", token):
            raise ValueError("Credential format")
except Exception:
    raise SystemExit("Registration is uncertain. Do not retry automatically.") from None
try:
    with os.fdopen(os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600), "w") as output:
        output.write(token + "\n")
except OSError:
    raise SystemExit("Access was created but saving failed. Resolve storage before retrying.") from None
print("Credential saved to protected storage. Its value was not displayed.")
PY
```

## 2. Connect without a local server

Remote HTTP requires no local vt-mcp or Python installation. The optional terminal registration example above uses Python; browser registration is also available.

Merge the following into `~/.codex/config.toml`. Preserve other settings and use one `virustotal` entry. The snippet contains only paths and protected environment, input or file references.

```toml
[mcp_servers.virustotal]
tool_timeout_sec = 180
url = "https://ai.virustotal.com/mcp"

[mcp_servers.virustotal.env_http_headers]
x-apikey = "VTAI_MCP_TOKEN"
```

Start the client in Bash with the protected token loaded into its environment; its value is never a command argument. An already open application may not inherit this environment.

```bash
(
  set +x
  unset VTAI_MCP_TOKEN
  IFS= read -r VTAI_MCP_TOKEN < "$HOME/.config/vt-mcp/token" || test -n "$VTAI_MCP_TOKEN" || exit 1
  test -n "$VTAI_MCP_TOKEN" || exit 1
  export VTAI_MCP_TOKEN
  exec codex
)
```

Inspect /mcp inside the Codex session started with the launch block. For a standalone check, reuse the launch block with exec codex mcp list so it loads the same protected token. Keep your existing Codex login.

## 3. Verify and use

Confirm that your client discovers `get_file_report`, `get_url_report`, `get_domain_report`, `get_ip_report`, `get_analysis`, `submit_file`, `get_submission`, `submit_url`, `reanalyze_domain` and `reanalyze_ip`. Local stdio additionally offers `submit_local_file`; remote MCP also lists `submit_attachment` for attachment descriptors with a public HTTPS download_url. The optional fields are file_id, file_name and mime_type. The client must supply the original file's URL; never invent one. Native attachment availability depends on the client. Call `get_domain_report` once for `virustotal.com`. Check the returned source, analysis date, coverage and report link. This consumes a query. Tool discovery alone does not verify authenticated access.

For files, hash first and use `get_file_report`. Submit actual bytes only when the user explicitly requests the upload, using the sharing guidance above. A missing report is not permission to upload. `submit_local_file(path, expected_sha256=None)` accepts up to 32,000,000 bytes over stdio. `submit_file(sha256, content_base64)` accepts up to 24,000,000 decoded bytes over either transport. Remote HTTP cannot read a local path. Submitted content may be shared with the VirusTotal community and security partners; base64 also passes through your MCP host. Host permissions apply; the tools add no per-call confirmation argument.

If this client cannot transfer the file bytes for an explicitly requested upload, offer the existing VirusTotal upload page at https://www.virustotal.com/gui/home/upload under the same sharing guidance. Before uploading, manually check the file's actual hash in VirusTotal and upload only if unknown. After the user uploads the file, obtain its actual hash and look up its report. The report may not be available immediately; every repeated query consumes quota. A web upload creates no VTAI receipt. This alternative does not bypass client permissions, authentication or quota limits, and must not repeat an uncertain submission.

For submissions through this connection, keep the SHA-256. Recover an uncertain submission with `get_submission(sha256)` using the same connection or credential, without sending bytes again. `exists` means an existing report; `submitted` supplies an analysis ID. Read that ID using `get_analysis` within a finite polling budget. Unknown can remain unknown permanently; do not repeat submission merely to resolve uncertainty.

Only when the user explicitly requests URL submission or network reanalysis, use `submit_url(url, request_id)`, `reanalyze_domain(domain, request_id)` or `reanalyze_ip(ip, request_id)`. Generate and persist a canonical lowercase UUIDv4 request ID before calling. Use `get_submission(request_id=request_id)` to recover with the same connection, then `get_analysis(analysis_id, request_id=request_id)` to select the returned analysis. Receipt recovery accepts exactly one of `sha256` or `request_id`. Standard sharing applies. Do not automatically replay an uncertain POST; a deliberate later rescan uses a new request ID. OAuth requires `vt:reports:read` and `vt:network-analysis:write`; approve the new network permission when reconnecting an existing OAuth client. File submission permission does not include network analysis.

## Troubleshooting and limits

For repeated file checks, deduplicate hashes within the task and reuse reports while their age remains appropriate to your decision. Space requests to avoid bursts; token-based REST and MCP use the same quota. The [Python REST lookup example](https://ai.virustotal.com/examples/query_file_reports.py) implements deduplication and bounded quota retries; see the [API skill](https://ai.virustotal.com/skills/BASIC.md) for usage.

REST and MCP using the same Agent Token share quotas; unknown reports and upstream failures can consume admitted queries. For 429, respect `Retry-After` and keep the existing credential; registering another identity is not quota recovery. For 400, check conflicting or malformed authentication headers. For 401/403, check credential mapping, validity and permissions without displaying the value. `X-VTAI-Error: unexpanded_credential` means an x-apikey variable reference was sent literally. Load the protected token into the client environment and use its documented variable syntax; do not paste the token into chat or register again. This specific diagnostic applies to x-apikey. Check the endpoint and method for MCP HTTP errors; a tool's `not_found` is an unknown indicator, not evidence of safety. Only when the user explicitly requests an upload, submit actual file bytes with the existing submission tools; a hash cannot start an analysis. Network submissions and reanalysis also require the user's explicit request. A service failure is not a clean report. Avoid URLs containing private paths or query parameters; use a domain lookup when appropriate.

[Current access limits and browser setup](https://ai.virustotal.com/connect/mcp?client=codex&transport=http&auth=token) · [Agent API instructions](https://ai.virustotal.com/skills/BASIC.md) · [OpenAPI](https://ai.virustotal.com/openapi.json)

## Disconnect the token-based setup

Remove the `virustotal` entry and restart the client. A shared credential remains active in other clients. To disable it everywhere, use [Revoke access](https://ai.virustotal.com/connect/mcp?client=codex&transport=http&auth=token#revoke). The authenticated API is `DELETE /api/v3/agents/me/token`; success returns 204. Reconnect with the same credential while it remains active.
