Skip to main content
Glama
feldboy

vulnerability-registry-mcp

by feldboy

Vulnerability Registry — MCP Server

A natural-language access layer over a legacy CVE registry, built as an MCP server (TypeScript, official @modelcontextprotocol/sdk), plus an agent CLI (Phase 2) that answers analyst questions end-to-end.

The Problem & Who It Serves

The registry's data lives in undocumented pipe-delimited text files on an internal server — no API, no search. Two personas suffer daily:

  • Security analysts need instant lookups during live incidents ("What is the CVSS score of Log4Shell?") and filtered investigations ("Which Linux Kernel CVEs from the past year are still open?").

  • Risk managers need aggregate numbers for reporting ("How many critical vulnerabilities are still open?").

Success criterion: an analyst asks a free-form question and gets a correct, data-grounded, verifiable answer in seconds instead of minutes of manual file searching.

Related MCP server: MCP Cybersecurity Server

Quick Start

npm install
npm run build
npm test          # parser test suite

Connect to Claude Desktop

Add to claude_desktop_config.json (Settings → Developer → Edit Config):

{
  "mcpServers": {
    "vulnerability-registry": {
      "command": "node",
      "args": ["/ABSOLUTE/PATH/TO/vulnerability-registry-mcp/dist/server.js"]
    }
  }
}

Restart Claude Desktop and ask: "How many critical vulnerabilities are still open?"

The data directory defaults to ./data; override with a CLI argument (node dist/server.js /path/to/data) or the VULN_DATA_DIR env var.

Phase 2 — Agent CLI

export GEMINI_API_KEY=...   # free key: https://aistudio.google.com/apikey
npm run agent                                        # interactive
npm run agent -- "Which Linux CVEs are still open?"  # one-shot

The default model is the rolling alias gemini-flash-latest (resilient to model deprecations); override with the GEMINI_MODEL env var. The default Gemini Flash is a thinking model — for snappy answers on this registry's simple tool routing, GEMINI_REASONING=low cuts latency dramatically. DEBUG=1 prints raw LLM responses.

Swapping providers: the agent depends only on the OpenAI-compatible chat-completions format, so any such endpoint works via env vars alone:

# Groq
LLM_BASE_URL=https://api.groq.com/openai/v1 GEMINI_API_KEY=<groq-key> \
  GEMINI_MODEL=llama-3.3-70b-versatile npm run agent

# Ollama (local, no key needed — pass any non-empty string)
LLM_BASE_URL=http://localhost:11434/v1 GEMINI_API_KEY=ollama \
  GEMINI_MODEL=llama3.1 npm run agent

Caveat: tool-use quality varies by model — small local models route tools less reliably than hosted ones.

Web UI (stretch)

export GEMINI_API_KEY=...
npm run ui        # then open http://localhost:3000

A SOC-console chat (designed with Google Stitch) wired to the same agent module as the CLI — one agent, two frontends, mirroring the one-server many-clients principle of MCP. Two things worth noticing: the header stats (Open / Critical Open / Total) are live get_statistics MCP calls, not hard-coded numbers; and every tool call the agent makes is rendered as a chip in the conversation — the same transparency principle as the CLI's 🔧 lines, because an analyst should always see where an answer came from.

The CLI prints every tool call it makes (🔧) — an analyst should always see where an answer came from.

Tools

Tool

What it does

Example question

Persona

search_vulnerabilities

Combinable filters: free text, vendor, severity, status, date range. Paginated, newest first, always returns total.

"Which Linux CVEs from 2024 are open?"

Analyst (investigation)

get_vulnerability

Full record by CVE ID or common name ("Log4Shell").

"What's the CVSS of Heartbleed?"

Analyst (incident)

get_statistics

Counts grouped by severity / status / vendor, with the same filters. Returns numbers, not records.

"How many critical vulns are open?"

Risk manager

list_vendors

Full vendor list — lets the LLM resolve names to IDs.

(infrastructure for the other tools)

Four tools, deliberately few: each maps to a real analyst job, and the three example questions in the brief are each answerable in one or two calls.

How This Was Built — Process Notes

A note on how I actually worked, because the process was a deliberate choice.

I spent the first stretch of this assignment entirely away from code. Before deciding anything technical, I broke the problem down as a product problem: who actually suffers from data trapped in text files, what each persona asks in their worst moment (an analyst mid-incident vs. a risk manager before a board report), and whether a natural-language layer genuinely solves their problem better than a dashboard or a REST API would. I also treated the brief itself as evidence — its three example questions map one-to-one to personas, and its hints (the VERSION field, "thousands of records", the free-text version field) read as requirements, not trivia. The tool set was derived from those persona questions before a single line of implementation.

I built this with AI as a deliberate workflow, not a shortcut: I ran a structured product-thinking session with Claude (a dedicated PM prompt) to pressure-test personas, scope boundaries and tool design before development, then used it as a pair programmer during implementation. Every design decision documented in this README is one I made and can defend; the AI compressed the execution time into the assignment's 3–5 hour budget. Verification was mine too: unit tests for the parser, a protocol-level smoke test, an eval set with ground truths computed directly from the data, and live runs through both Claude Desktop and the agent CLI. The web UI was designed with Google Stitch and wired to the same agent module that powers the CLI.

Design Decisions

Dynamic format parsing — the VERSION hint. The parser reads the # FORMAT: metadata line at load time and maps fields by column name, never by position. Reordered or added columns in a future format version require zero code changes (covered by tests). Unknown major versions load with a loud warning rather than crashing — an analyst is better served by partial data plus a warning than by a dead server. Malformed rows are skipped, counted, and reported with line numbers: never crash, never hide.

Built for thousands of records — but the real constraint is the LLM's context. In-memory linear scans are microseconds at this scale; the real risk is a tool dumping hundreds of records into the model's context. Every record-returning tool paginates (default 20, max 100) and always reports the total match count, and get_statistics exists so counting questions cost a few dozen tokens instead of a full listing.

Primitives, not reports. There is no generate_weekly_report tool on purpose. Tools expose precise data primitives; the LLM composes them into any report, for any audience, in any format. Baking presentation into the data layer would turn every report-format change into a deployment.

Why MCP + tools, not RAG. The data is structured and the questions demand exactness — counts, filters, cross-references. Embedding retrieval answers "approximately"; a risk manager asking "how many critical open?" needs the number. Tools return ground truth; the LLM only narrates it. MCP specifically decouples the data layer from the AI layer: same server, any client, swappable model.

Faithful to the source. The client's files are the source of truth. The server flags structural problems (orphan vendor_ids, invalid severities, non-numeric scores) at load time but never silently "fixes" data — a system that corrects records behind the analyst's back forfeits trust in every answer.

SDK v1 stable, not v2 beta. The official SDK's v2 is in beta at the time of writing. An internal server for security teams shouldn't ride a beta; dependencies are pinned to v1 with the current registerTool API.

Errors that steer the agent. Error messages are written as prompts: Unknown vendor_id "OpenSSL". Known vendors: V1=Microsoft, … Call list_vendors for details. The agent self-corrects in one round instead of flailing. Zod schemas reject malformed parameters with equally actionable messages.

stdio discipline. On stdio transport, stdout is the JSON-RPC channel. All logging goes to stderr — there is not a single console.log in the server path.

Resilience against a moving provider. Three real-world failures surfaced during development, and each got a permanent fix rather than a workaround: a deprecated model name (→ default to Google's rolling gemini-flash-latest alias, overridable), a silently hanging request (→ 90s timeout that fails loudly), and provider overload 503s (→ exponential-backoff retry on 429/503, visible to the user, non-retryable errors thrown immediately). Same principle as the dynamic parser: don't assume today's external contract holds tomorrow.

Phase 2 talks MCP, not imports. The agent connects to the Phase 1 server as a subprocess through the real protocol (client + stdio) and discovers tools dynamically via listTools. Importing the store directly would have been easier — and would have proven nothing about the server. The agent core is also provider-portable: it depends only on the OpenAI-compatible chat-completions format (Gemini today; Groq/OpenAI is a base-URL change) and is frontend-agnostic (the CLI is a thin wrapper; a web UI would reuse the same module).

Data Quality Observations

Structural validation passes cleanly on the provided files — but human review found semantic misattributions the system cannot detect on its own, since the referenced vendor IDs exist:

  • CVE-2024-21762 (Fortinet SSL VPN) is attributed to V4 = Google

  • CVE-2024-27198 (TeamCity, a JetBrains product) is attributed to V1 = Microsoft

Per the "faithful to the source" rule, the server reports the data as-is. This structural-vs-semantic gap is exactly what the NVD cross-checking idea below would close.

In practice, this design paid off: when queried through Claude Desktop, the model flagged both misattributions on its own — the faithful-to-source server hands over ground truth, and the LLM layer catches semantic issues that structural validation cannot.

Business Impact (ROI)

Conservative estimate: a manual lookup in raw text files takes ~10 minutes (locate file, grep, cross-reference vendor). A team of 5 analysts averaging 6 queries a day spends ~5 analyst-hours daily on lookups — roughly 1,200 analyst-hours a year reduced to seconds per query. The harder-to-quantify value is larger: during a live incident (a Log4Shell-class disclosure), time-to-answer for "are we exposed, and where?" drops from hours of manual searching to a single question — and incident exposure time is the most expensive time there is.

Evaluation

eval/questions.md contains 10 questions with ground-truth answers computed directly from the data (via scripts/truth.ts), including the brief's three example questions and multi-step questions that require chained tool calls (list_vendorssearch_vulnerabilities). A protocol-level smoke test (scripts/smoke.ts) exercises the server through a real MCP client:

TOOLS: list_vendors, get_vulnerability, search_vulnerabilities, get_statistics
Q1 (CVSS of Log4Shell):        10.0
Q2 (critical still open):      2  {"critical": 2, "high": 2 open in total}
Q3 (Linux Kernel CVEs):        4  CVE-2024-1086, CVE-2023-35829, CVE-2022-0847, CVE-2016-5195
ERR (steering check):          Unknown vendor_id "OpenSSL". Known vendors: V1=Microsoft, ...

Real agent transcript (Gemini, multi-tool chain chosen by the model on its own):

❓ How many critical vulnerabilities are still open?
  🔧 get_statistics({"group_by":"severity","status":"open"})
  🔧 search_vulnerabilities({"status":"open","severity":"critical"})

Based on the registry, there are 2 critical vulnerabilities that are still open:
1. CVE-2024-27198 (TeamCity Auth Bypass) — CVSS Score: 9.8
2. CVE-2024-21762 (Fortinet SSL VPN OOB) — CVSS Score: 9.6

Note the model's own strategy: it answered the count from get_statistics (cheap) and then enriched with search_vulnerabilities for the specific CVE IDs — the two primitives composing exactly as designed. Implementation detail that matters: the agent echoes the assistant message back verbatim, preserving Gemini's thought_signature reasoning chain across tool rounds.

Multi-step question (name-to-ID resolution chosen by the model on its own):

❓ Which Linux Kernel vulnerabilities are still open?
  🔧 list_vendors({})
  🔧 search_vulnerabilities({"status":"open","vendor_id":"V5"})

There are 2 open vulnerabilities for the Linux Kernel (V5):
1. CVE-2024-1086 "Linux Netfilter UAF" — high (7.8), kernel 5.14-6.6
2. CVE-2023-35829 "Linux Race Condition" — high (7.0), kernel < 6.3.2

Architecture

flowchart LR
    subgraph Data layer
        F[".db files<br/>(pipe format)"] --> P["parser.ts<br/>dynamic FORMAT mapping"]
        P --> S["store.ts<br/>in-memory + query primitives"]
    end
    subgraph MCP server
        S --> T["tools.ts<br/>4 tools, Zod schemas"]
        T --> M["server.ts<br/>stdio transport"]
    end
    M <-->|MCP / JSON-RPC| C1["Claude Desktop"]
    M <-->|MCP / JSON-RPC| C2["Agent CLI (Phase 2)<br/>Gemini via OpenAI-compat API"]

Each layer knows only the one beneath it: the parser knows nothing about MCP, the tools know nothing about file formats. Moving to PostgreSQL means replacing store.ts and nothing else.

What I'd Build With More Time

  1. NVD cross-checking — compare each record against the official NIST registry and flag divergences (wrong vendor attribution, changed severity, newly available patches). Directly addresses the semantic data issues found above.

  2. Exploit-aware prioritization (EPSS / CISA KEV) — the registry ranks by CVSS alone, which modern vulnerability management considers insufficient: most CVSS-critical vulns are never exploited in the wild. Enriching records with an EPSS probability and a KEV flag would point analysts at what is actually being exploited.

  3. Alerting layer — push a notification when a new critical vulnerability enters the registry, instead of waiting to be asked.

  4. Scheduled digest — a weekly summary for the risk manager, composed by the agent from get_statistics calls.

  5. Structured affected_versions — parse the free-text version ranges to support "is version X vulnerable?" queries. Deliberately skipped in this iteration: the field is free text by design, and a partial parser would give confidently wrong answers.

  6. A small web UI over the same agent module (it is already frontend-agnostic).

Assumptions

  • The client's files are read-only inputs and the single source of truth; the access layer never mutates or corrects them.

  • ISO dates in the data (YYYY-MM-DD) — string comparison is used for ranges.

  • severity and status vocabularies are as documented (plus validation warnings if a future file deviates).

  • Claude Desktop fulfills the "connectable via an MCP client" requirement; the agent CLI is submitted as Phase 2 on top of it.

F
license - not found
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/feldboy/vulnerability-registry-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server