VulnSea
← All posts

Wiring VulnSea into an agent: a hands-on tutorial

· VulnSea
tutorialagentsapimcp

Most vulnerability databases are built for a human with a browser. VulnSea is built so a program — a cron job, a CI step, or a tool-use LLM — can read the whole corpus without scraping HTML or parsing prose. Every CVE is structured markdown with stable frontmatter, mirrored as JSON, and served through an incremental, cache-aware API plus a native MCP endpoint.

This post is a tutorial. By the end you'll have a working triage loop, an SBOM check, and an MCP server your model can call directly — and you'll see why each piece is cheaper to run against VulnSea than against a raw feed.

Everything below works today and the whole corpus is open — no account needed to read. An API key raises your rate limit (60/min anonymous → 600/min keyed). Grab a key from your account and export it:

export AUTH="Authorization: Bearer vsk_your_key_here"

1. The one number your loop should sort on: depth score

Base CVSS overstates urgency — a critical nobody is exploiting outranks a high that's already in the wild. VulnSea attaches a depth score to every record: a 0–100 composite that blends impact (CVSS/severity), likelihood (EPSS), and proof of exploitation (KEV, 0day, in-the-wild, public PoC). It deliberately ranks an exploited high above a quiet critical.

Ask for just the fields you need and let the score do the prioritizing:

curl -s -H "$AUTH" \
  "https://vulnsea.com/api/cve?severity=critical,high&fields=id,severity,depthScore,depth,kev,epss&limit=20"
{
  "count": 20,
  "access": "full",
  "results": [
    { "id": "CVE-2026-34910", "severity": "critical", "depthScore": 97, "depth": "hadal", "kev": true, "epss": 0.94 },
    { "id": "CVE-2026-34712", "severity": "high", "depthScore": 71, "depth": "abyssal", "kev": true, "epss": 0.61 }
  ]
}

depth is the coarse triage zone (sunlit → twilight → midnight → abyssal → hadal); depthScore is the fine-grained number. Sort your work queue on depthScore descending and you're patching in real-world-risk order, not base-score order. The fields parameter trims the payload to exactly those keys — fewer tokens when the response feeds an LLM.

2. Poll only what's new (and pay nothing when idle)

The expensive way to stay current is re-fetching everything. VulnSea gives you a high-water mark and a cursor, so an agent only ever processes deltas.

# First run: newest-last, page on `next` until it's null. Save the last id/time.
curl -s -H "$AUTH" \
  "https://vulnsea.com/api/cve?since=2026-06-30T00:00:00Z&order=asc&limit=100"

Each response carries a next cursor and every row a stable ingestedAt. Store the newest one; next run, pass it as since. Between runs, use the cheap probe or a conditional request so an idle poll costs almost nothing:

# HEAD gives you the corpus size + newest ingest time in headers — no body.
curl -sI -H "$AUTH" "https://vulnsea.com/api/cve" | grep -i x-max-ingested-at

# Or send the previous ETag and get a 304 when nothing changed.
curl -s -H "$AUTH" -H "If-None-Match: \"$ETAG\"" \
  "https://vulnsea.com/api/cve?since=$HWM&order=asc"
#   304 => sleep    |    200 => new records to process

Why it matters: a poll every few minutes is basically free until something actually lands. That's what makes tight schedules affordable.

3. A complete triage loop

Here's the whole pattern in one script — poll the delta, gate on structured fields (no LLM tokens spent on noise), then deep-read only the survivors.

#!/usr/bin/env bash
set -euo pipefail
AUTH="Authorization: Bearer $VULNSEA_KEY"
HWM=$(cat .hwm 2>/dev/null || echo "2026-06-01T00:00:00Z")

# Server-side narrowing: only critical/high that are actually being exploited,
# oldest-first so we can advance the mark safely. ndjson = one CVE per line.
curl -s -H "$AUTH" \
  "https://vulnsea.com/api/digest?since=$HWM&severity=critical,high&exploited=true&order=asc&format=ndjson" \
| while IFS= read -r line; do
    id=$(echo "$line" | jq -r .id)
    score=$(echo "$line" | jq -r .depthScore)
    # Only escalate the genuinely urgent — depth score is the single gate.
    if [ "$score" -ge 70 ]; then
      # Deep-read the canonical markdown; feed it straight to your model/ticketer.
      curl -s -H "$AUTH" "https://vulnsea.com/cve/$id.md" > "/tmp/$id.md"
      echo "ESCALATE $id (depth $score)"
    fi
    echo "$line" | jq -r .ingestedAt > .hwm   # advance the high-water mark
  done

The gate is pure structured data — severity, exploited, depthScore — so you never pay for a model call on the 95% that don't matter. The .md you fetch for survivors is narrative the model can cite verbatim.

4. "Am I affected?" — match your dependencies

The highest-leverage question isn't "what's new" — it's "does any of this hit my stack." Post a dependency list to /api/sbom and get back the CVEs per component, ranked by depth, each tagged with whether your version is in range and the fixed version to jump to.

curl -s -H "$AUTH" -H "content-type: application/json" \
  https://vulnsea.com/api/sbom \
  -d '{"components":["pkg:npm/[email protected]","pkg:pypi/[email protected]","log4j-core 2.14.1"]}'
{
  "summary": { "components": 3, "resolved": 3, "vulnerable": 3, "matches": 7 },
  "results": [
    {
      "name": "log4j-core", "version": "2.14.1",
      "matches": [
        { "id": "CVE-2021-44228", "depth": "hadal", "versionMatch": "in-range",
          "fixedVersion": "2.15.0", "kev": true }
      ]
    }
  ]
}

Accepts purls (pkg:npm/[email protected]) or {ecosystem,name,version} objects. Point it at a package.json, requirements.txt, go.mod, or lockfile in CI and fail the build on an in-range hadal match. No account? The same engine backs the no-signup Am I affected? tool — paste a manifest and see results in ten seconds.

5. Skip the HTTP glue: the MCP endpoint

If your agent runtime speaks the Model Context Protocol (Claude, Cursor, and friends), you don't need to hand-write any of the above. VulnSea ships a native MCP server at /api/mcp exposing list_cves, search_cve, get_cve, batch_get_cve, triage_sbom, related_cve, and get_stats — same auth and tiering as the REST API, and every result carries the depth + depthScore.

{
  "mcpServers": {
    "vulnsea": {
      "url": "https://vulnsea.com/api/mcp",
      "headers": { "Authorization": "Bearer vsk_your_key_here" }
    }
  }
}

Now your model can answer "are we exposed to anything like Log4Shell, and which of our services run it?" by calling search_cve then triage_sbom itself — no integration code, no OpenAPI codegen, no prompt describing endpoints.

6. Let the spec write your client

Prefer a typed client or your own tool definitions? Point any tool-use model or codegen at the OpenAPI 3.1 spec and the machine manifest at /llms.txt. Both describe the full surface — auth, paging, the fields/ids/probe cost knobs, and every field's meaning — so you generate the integration instead of writing it.

Why VulnSea, specifically

  • One defensible priority number. depthScore folds CVSS + EPSS + real exploitation into a 0–100 you can sort on. Sort the UI feed by it with sort=risk.
  • Delta-first and cache-aware. since + cursors + 304 mean frequent polling is cheap, and your loop only touches what changed.
  • Structured, then narrative. Gate on typed fields for free, deep-read the .md only when it earns the tokens.
  • Agent-native, not bolted on. REST, OpenAPI, /llms.txt, and a hosted MCP server — pick the integration depth you want.
  • Answers "am I affected," not just "what exists." SBOM triage turns the corpus into a checklist against your actual dependencies.

Start with the About page for the full workflow patterns, grab a key on your account, and check freshness anytime on the status page.

Sound the depths → vulnsea.com

Wiring VulnSea into an agent: a hands-on tutorial · VulnSea