Inventory Guard MCP Server
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Inventory Guard MCP Serverrun a scan on my inventory and show new findings"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Inventory Guard (invguard)
A self-hosted vulnerability watchdog for the hardware and software you actually own.
You tell it what's in your rack. It watches the public CVE firehose — NVD, CVE List V5, OSV, CISA KEV, EPSS, Cisco PSIRT, MSRC, CSAF vendors — and tells you when something that matters to your boxes shows up. Nothing else.
It runs in one container, on one machine, behind loopback. It never scans your network. It never can.
Version 1.7.2 · Python 3.12 / FastAPI / SQLite · Docker Compose · MCP server included
Note on language: the web UI, CLI output and error messages are in Ukrainian. The codebase, API and this document are in English. Internationalization is on the roadmap, not in the box.
Table of contents
Related MCP server: SAST MCP Server
The design stance
Most vulnerability tooling starts by scanning your network. Inventory Guard starts by refusing to.
The inventory is maintained by hand — web forms, CSV import, or an AI agent acting on your explicit instruction. There is no discovery, no SNMP, no SSH polling, no agents on endpoints. This isn't a missing feature; it's the product. A security tool that opens connections into your infrastructure is a security tool that can be turned against it.
Three invariants follow from that, and they're enforced in code and in tests:
Invariant | Enforcement |
The app never initiates connections into your internal network |
|
Only public identifiers leave the machine | Outbound URL and body are scanned; anything matching |
A feed failure degrades, never cascades | Each connector is wrapped; a failure lands in |
What actually goes over the wire: CPE strings, PURLs, version numbers, package names. Public classifiers. Nothing that describes your topology.
What it does
Manual inventory of devices (vendor / model / OS family / version → CPE 2.3) and software packages (name / ecosystem / version → PURL), with cascading autocomplete backed by the official NIST CPE dictionary and ecosystem registries.
Incremental feed sync — full CVE corpus on first run, cursor-based deltas after that.
Version-range matching across CPE ranges and ecosystem version schemes, with source priority
vendor > NVD > OSVand provenance recorded per match.Scheduled, triggered and manual scans — weekly by default, 60-second debounce after inventory edits, or on demand.
Markdown reports stored in SQLite with a one-way file mirror, plus a diff of new/closed findings per run.
Alerts on KEV membership, CVSS ≥ 7.0 or EPSS ≥ 0.5 (all configurable), deduplicated per finding.
Findings workflow —
open → acknowledged → fixed | false_positive, append-only history.MCP server so Claude Desktop or any other agent can read the report, run a scan and triage findings.
CSV/JSON export and a full database dump endpoint.
Architecture
One process, one SQLite file, three ways in.
graph TB
subgraph clients["Clients"]
UI["Web UI<br/><i>vanilla ES modules</i>"]
CURL["REST clients<br/><i>scripts, CI</i>"]
AGENT["AI agents<br/><i>Claude Desktop, IDEs</i>"]
end
subgraph app["invguard container - 127.0.0.1:8480"]
MW["Bearer-token middleware<br/><i>everything except /health and /</i>"]
ROUTERS["FastAPI routers<br/><i>thin: zero logic</i>"]
MCP["FastMCP server<br/><i>/mcp + stdio shim</i>"]
subgraph domain["Domain"]
INV["inventory<br/>service, importer, catalog"]
SCAN["compliance<br/>scanner, reporter, alerts, scheduler"]
MATCH["matching<br/>engine, versions"]
KB["kb<br/>worker, store, sources"]
FEEDS["feeds<br/>connector registry"]
end
GUARD["httpguard<br/><i>the only door outward</i>"]
DB[("SQLite WAL<br/>invguard.db<br/><i>0600, self-migrating</i>")]
end
EXT["Public feeds<br/><i>NVD, CVE V5, OSV, KEV, EPSS,<br/>Cisco, MSRC, CSAF</i>"]
FS["/reports<br/><i>one-way markdown mirror</i>"]
UI --> MW
CURL --> MW
AGENT --> MCP
MW --> ROUTERS
ROUTERS --> INV
ROUTERS --> SCAN
ROUTERS --> MATCH
ROUTERS --> KB
MCP --> INV
MCP --> SCAN
MCP --> MATCH
SCAN --> FEEDS
KB --> GUARD
FEEDS --> GUARD
GUARD --> EXT
INV --> DB
SCAN --> DB
MATCH --> DB
KB --> DB
FEEDS --> DB
SCAN --> FSWhy these choices, briefly. SQLite because this is a single-user local service and one file is a trivial backup — access goes through a repository layer so a PostgreSQL move wouldn't touch the domain. Vanilla JS because a security tool shouldn't ship a build chain and a node_modules supply chain with it. One uvicorn worker because a shared DB connection and a shared scheduler beat horizontal scaling for a workload measured in dozens of inventory items. No Celery/Redis for background jobs — the queue lives in the same SQLite database (kb_jobs) with an asyncio worker in the app lifespan, which survives restarts without two extra containers.
How a scan actually runs
Three triggers, one pipeline, exactly one run at a time.
flowchart TD
T1["cron<br/><i>every N hours, default 168</i>"] --> LOCK
T2["inventory changed<br/><i>60s debounce</i>"] --> LOCK
T3["manual / scan_now<br/><i>UI, REST, agent</i>"] --> LOCK
LOCK{{"asyncio.Lock + flock<br/><i>single-flight across processes</i>"}}
LOCK --> RUN["INSERT scan_runs<br/><i>trigger, scope</i>"]
RUN --> FACTS["Phase 1 - FACT feeds, sequential<br/>cvelist_v5, cisco_openvuln, msrc,<br/>csaf, nvd, osv"]
FACTS --> STORE[("advisories + affected<br/><i>cached corpus</i>")]
STORE --> ENGINE["matching.engine.sync()<br/><i>exactly once</i>"]
ENGINE --> DIFF["diff vs previous run<br/><i>new, closed, total_open</i>"]
DIFF --> ENRICH["Phase 2 - ENRICHMENT feeds<br/>cisa_kev, epss, premium<br/><i>only for advisories that actually matched</i>"]
ENRICH --> FINISH["UPDATE scan_runs<br/><i>feeds_ok / feeds_failed snapshot</i>"]
FINISH --> REPORT["reporter.generate()<br/><i>markdown to DB + atomic file mirror</i>"]
REPORT --> ALERTS["alerts.process()<br/><i>KEV, or CVSS 7.0+, or EPSS 0.5+</i>"]
FACTS -. "connector raises" .-> FAIL["feeds_failed[]<br/><i>run continues on cache,<br/>report marks the stale source</i>"]
FAIL -.-> ENGINETwo ordering decisions worth calling out. Facts before enrichment: KEV and EPSS are asked only about advisories that already matched something you own — no point pulling exploit probability for CVEs irrelevant to your rack. Matching runs exactly once, between the phases; enrichment doesn't change affected, so a second sync would only zero out the "new findings" counter.
Passing target_ids to scan_now narrows the run to specific inventory items — seconds instead of minutes, useful right after adding one device.
Data sources
Connector | Source | Auth | Role |
| CVE List V5 (MITRE bulk releases) | none | Core corpus, incremental by release cursor |
| NVD API 2.0 | optional free key | CPE configurations with version ranges — the backbone of hardware matching |
| OSV.dev | none | Software packages by PURL (PyPI, npm, Go, Maven, deb, rpm) |
| CISA Known Exploited Vulnerabilities | none | "Exploited in the wild" flag — always alerts |
| FIRST.org EPSS | none | Exploitation probability 0–1, drives report ordering |
| Cisco PSIRT openVuln | OAuth2 (free registration) | Queries by exact IOS/IOS-XE/NX-OS/ASA version — more precise than CPE matching, returns first-fixed |
| Microsoft MSRC | none | Windows: matches against |
| Any CSAF 2.0 provider | none | One parser covers Dell, VMware, Juniper, Fortinet, Siemens… configure provider URLs in settings |
| Vulners (premium) | API key | Vendor bulletin aggregation, exploit markers |
| VulnCheck (premium) | API key | Extended KEV, exploit intelligence |
Without an NVD API key you're rate-limited to roughly 5 requests per 30 seconds and the first corpus build takes a long while. The key is free and takes two minutes: https://nvd.nist.gov/developers/request-an-api-key. Roughly a tenfold speedup.
Premium connectors are dark by default. They don't appear in a run, don't register their egress domains, and can't open a socket until you enter every required key. Deleting the key disables them instantly, no restart. Data from them only enriches records (enrichment_json) — a match found solely by a premium source is tagged matched_by='<source>' and badged in the UI, so every decision stays auditable.
MikroTik RouterOS and Supermicro BMC/BIOS have no vendor API. They're covered through NVD CPE + CVE List V5, and the report says so honestly: matched_by='nvd_cpe'.
The matching pipeline
flowchart LR
subgraph inv["Your inventory"]
D["Device<br/><i>cisco / c9300 / ios_xe / 17.6.4</i>"]
P["Package<br/><i>pypi / requests / 2.28.1</i>"]
end
D --> CPE["CPE 2.3<br/><i>cpe:2.3:o:cisco:ios_xe:17.6.4</i>"]
P --> PURL["PURL<br/><i>pkg:pypi/requests@2.28.1</i>"]
AFF[("affected<br/>cpe_or_purl + version_range_json")]
CPE --> CMP{"versions.compare()<br/><i>range boundaries, epochs,<br/>release suffixes,<br/>Including / Excluding</i>"}
PURL --> CMP
AFF --> CMP
CMP -->|hit| M["match<br/><i>matched_by = vendor / nvd_cpe / osv / csaf</i>"]
CMP -->|no hit| SKIP["ignored"]
M --> PRIO{"conflicting sources?"}
PRIO -->|"vendor beats nvd beats osv"| KEEP["keep the most authoritative<br/><i>the vendor knows its own fixed versions</i>"]Version comparison is the part that quietly decides whether the whole product is useful. Boundary conditions (versionEndExcluding vs versionEndIncluding), RPM epochs, release suffixes and vendor formats like 15.2(2)E each have dedicated tests — a false "no vulnerabilities" is the worst possible output of a tool like this.
Finding lifecycle
stateDiagram-v2
[*] --> open : matcher finds it
open --> acknowledged : you take it on
open --> fixed : version bumped
open --> false_positive : comment required
acknowledged --> fixed
acknowledged --> false_positive : comment required
fixed --> open : reappears in a later run
false_positive --> [*]
fixed --> [*]
note right of open
Every transition is appended to
match_history. Nothing is ever
overwritten.
end noteDeleting a device closes its open findings with a "device decommissioned" marker rather than dropping the rows.
Egress guard
Every outbound request — including each hop of a redirect chain — passes three independent checks before a socket is opened.
flowchart TD
REQ["Outbound request<br/><i>from a connector or the KB worker</i>"] --> C1{"1. Host on the allowlist?<br/><i>core domains + egress_domains<br/>of ENABLED connectors only</i>"}
C1 -->|no| BLOCK["GuardError<br/><i>reason surfaces in the run report</i>"]
C1 -->|yes| C2{"2. Literal IP, or resolves to<br/>private / loopback / link-local?"}
C2 -->|yes| BLOCK
C2 -->|no| C3{"3. Do URL or body contain<br/>name / role / notes / host_label<br/>or any IP address?"}
C3 -->|yes| BLOCK
C3 -->|no| SEND["Send"]
SEND --> REDIR{"redirect?"}
REDIR -->|yes| C1
REDIR -->|no| DONE["Response"]Check 3 has a subtlety that bit us in 1.0.1: a device with the role dell or the name cisco was blocking every legitimate NVD request for that vendor. The guard now maintains a set of public tokens — CPE components, PURL components, vendor and OS-family dictionary entries — that are legal to send outward.
The single documented exception to the private-address rule is the alert webhook, and it's restricted to loopback: alerts are not permitted to leave the machine.
Installation
Requirements
Docker with Compose v2 (
docker compose version), or the legacydocker-composebashandcurlon the host — the install scripts need both. Most distributions ship them; Alpine and some minimal container images don't (apk add bash curl)~2 GB free disk for the CVE corpus, 1 GB RAM available to the container
Outbound HTTPS to the feed domains
On native Linux, your user must be able to talk to the Docker socket. If docker info fails with permission denied:
sudo usermod -aG docker "$USER" # then log out and back in, or: newgrp dockerinstall.sh checks this up front and prints that command rather than failing later with something unrelated.
Quick start
git clone https://github.com/<you>/invguard.git
cd invguard
bash install.shinstall.sh checks Docker and curl, creates data/, reports/, upd/, backups/, writes .env, builds and starts the container, waits for health, and prints your access token. It's idempotent — re-running it never loses data.
Why there's a generated .env
This is the one piece of setup that genuinely differs between platforms, so it's worth thirty seconds of your attention.
The container writes to ./data and ./reports through bind mounts. On Docker Desktop (macOS, Windows) the file-sharing layer rewrites ownership, so whatever UID runs inside the container appears as you on the host. On native Linux there is no such translation — UIDs are literal. A container running as UID 10001 simply cannot write into a data/ directory that your user created with mode 700, and the failure is ugly: SQLite can't create the database, the app dies at startup, no token is ever generated.
So install.sh writes your host UID/GID into .env, and compose.yaml interpolates them:
INVGUARD_UID=1000
INVGUARD_GID=1000user: "${INVGUARD_UID:-10001}:${INVGUARD_GID:-10001}"Consequences worth knowing:
.envis machine-specific and gitignored. Cloning the repo elsewhere means runninginstall.sh(or copying.env.exampleand filling inid -u/id -g) — bringing someone else's.envalong will break the mount.Running
install.shas root deliberately does not run the container as root: it keeps UID 10001 andchowns the volumes to match.If you ever start the stack by hand without
.envpresent, it falls back to UID 10001 and you're back to the Linux permission problem.docker compose upalone is only safe once.envexists.
Manual
docker compose up -d --build
docker compose ps # invguard: Up (healthy)
cat data/.token # your token
curl http://127.0.0.1:8480/healthThen open http://127.0.0.1:8480 and paste the token when prompted. The browser stores it locally; you won't be asked again.
A word on exposure
The port binds to 127.0.0.1 only. Publishing it to a LAN is a deliberate act, and if you do it, put TLS in front — your own reverse proxy or Tailscale. Don't just flip the compose mapping to 0.0.0.0 and call it a day: the token is the only authentication layer, and it was designed for a loopback threat model.
First run
sequenceDiagram
actor You
participant UI as Web UI
participant KB as KB worker
participant Scan as Scanner
participant Feeds as Public feeds
You->>UI: paste token
You->>UI: settings - add NVD API key
You->>UI: "+ Add vendor" (e.g. Cisco)
UI->>KB: enqueue job (202)
KB->>Feeds: page through the NIST CPE dictionary
KB-->>UI: progress: vendors, models, OS, versions
You->>UI: add device - autocomplete fills the CPE
UI->>Scan: inventory changed (60s debounce)
Scan->>Feeds: full corpus sync - minutes, once
Scan-->>UI: report + findingsRecommended order:
⚙ → NVD API key. Do this first; everything downstream is faster.
⚙ → "+ Add vendor" for each vendor in your rack. This pulls that vendor's slice of the official CPE dictionary into a local knowledge base, so autocomplete only ever offers identifiers that will actually match. Runs in the background with a progress bar; a giant vendor is capped at 120k entries.
Inventory → + Add devices and packages. Vendor → model → OS → version cascades; the CPE is assembled for you and validated before saving. Or import CSV — you get a per-row preview with reasons for every rejected line before anything is written, and partial success ("added N, rejected M").
Report → "Scan now". The first run downloads the full CVE corpus — hundreds of MB, expect minutes. Later runs are incremental.
bash scripts/setup-mcp.shif you want an agent connected.
The suggestion order in autocomplete is deliberately precision-first: registry version → exact OS build → CPE marketing label. For Windows 10 you'll be offered 10.0.19045.5011 before 22h2, because the build number is what actually determines whether a patch is installed.
Configuration
Environment (compose.yaml)
Variable | Default | Meaning |
|
| SQLite, token file, CVE mirror |
|
| Markdown report mirror |
|
| Where the plaintext token is written once |
| host | Read by Compose, not by the app — see |
There is no port environment variable. The port inside the container is fixed at 8480 by the Dockerfile CMD and the healthcheck. To change the port you reach the service on, edit the mapping in compose.yaml — the first number is the host side:
ports:
- "127.0.0.1:9000:8480" # http://127.0.0.1:9000Keep the 127.0.0.1: prefix unless you've read A word on exposure.
Settings (⚙ in the UI, stored in kv)
Setting | Default | Notes |
|
| 1 hour – 90 days. Counted from the last run of any kind |
|
| |
|
| KEV always alerts regardless |
| (empty) | Loopback hosts only |
| (empty) | Masked on read — last 4 characters only |
| (empty) | Enables the openVuln connector |
| (empty) | Comma-separated |
| (empty) | Enables premium connectors |
|
| Request budget per premium connector per day |
Secrets entered in the UI take priority over the environment, are stored in the kv table, are masked on read and never appear in logs.
MCP integration
The same domain, exposed as agent tools. Two transports: streamable HTTP mounted at /mcp behind the same bearer token, and a stdio shim for desktop clients.
bash scripts/setup-mcp.sh # writes the Claude Desktop config, keeps existing serversThe script backs up your config and always writes it if Docker and the container are found — problems are reported as warnings rather than aborting halfway (a lesson from 1.7.1, where a fragile handshake check meant the config was sometimes never written at all).
Manual config, if you'd rather:
{
"mcpServers": {
"invguard": {
"command": "/usr/local/bin/docker",
"args": ["exec", "-i", "invguard", "python", "-m", "invguard.mcp_stdio"]
}
}
}Use the absolute path to your docker binary (command -v docker) — desktop clients don't inherit your shell PATH. Config locations: ~/Library/Application Support/Claude/claude_desktop_config.json (macOS), %APPDATA%\Claude\ (Windows), ~/.config/Claude/ (Linux). Fully quit and relaunch the client afterwards — not just close the window.
Tools: inventory_add_device, inventory_add_package, inventory_import, inventory_list, scan_now, compliance_status, get_report, query_cve, ack_match, resolve_match.
Resource: report://latest — the agent can read the latest report without calling a tool.
Verify readiness from inside the container:
docker exec -i invguard python -m invguard.cli mcp-check --verboseREST API
Everything the UI does, the API does. All routes except /health and / require Authorization: Bearer <token>.
TOKEN=$(cat data/.token)
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8480/api/summaryMethod | Path | Purpose |
|
| Liveness ( |
|
| Device CRUD |
|
| Package CRUD |
|
| CSV import ( |
|
| CSV template |
|
| Vendor / OS-family dictionary |
|
| Run now (optional |
|
| Run history with feed snapshots |
|
| Findings, filtered |
|
| Triage transition |
|
| Markdown |
|
| Rebuild the file mirror from the DB |
|
| Feed sync and state |
|
| Thresholds, keys, schedule |
|
| Knowledge base and autocomplete |
|
| CSV / JSON / ZIP |
Domain errors come back as 400 with a human-readable error field (in Ukrainian). A missing or bad token is 401.
CLI
docker compose exec invguard python -m invguard.cli <command>Command | What it does |
| Schema version, DB size, inventory counts, corpus size, findings by status, last run, per-feed freshness |
| Consistent snapshot via |
| Restore a snapshot; the current DB is renamed to |
| New token, old one invalid immediately, no restart |
| Regenerate the |
| MCP self-diagnosis: import, tool count, stdout cleanliness, installed SDK version |
The token's hash lives in the database, so a restored backup keeps working with the same token.
Backup, restore and updates
Backup is a copy of data/. For a consistent snapshot of a running system use invguard backup rather than cp — VACUUM INTO won't catch the database mid-write.
Updating from git:
git pull
docker compose up -d --buildMigrations run automatically on connect, keyed by PRAGMA user_version (currently v9). The migration list is append-only — a newer binary upgrades an older database; the reverse is not supported, so take a snapshot first.
Updating from a zip (for air-gapped or non-git deployments) — drop the archive in upd/ and run bash update.sh. It snapshots the database, backs up the current code, swaps it, rebuilds, and rolls back automatically if the build fails or health doesn't come up. data/, reports/, upd/ and backups/ are never touched.
After any update, hard-refresh the browser once (Ctrl+F5 / Cmd+Shift+R) and fully restart your MCP client — the MCP process lives inside the container and went down with it.
Troubleshooting
Install and startup
docker: command not found / "Docker не відповідає"
Docker isn't installed or the daemon isn't running. On Linux: sudo systemctl start docker. On macOS/Windows: launch Docker Desktop and wait for it to go green.
"compose.yaml не знайдено" Run the script from the repository root, not from a subdirectory.
The service doesn't come up within 60 seconds
install.sh now dumps the last 30 log lines automatically and names the three usual causes — you shouldn't have to go looking. To fetch more:
docker compose logs invguard --tail 100Usual suspects: the volume isn't writable by the UID in .env (unable to open database file); port 8480 is taken (ss -tlnp | grep 8480, or lsof -i :8480 — note ss isn't installed on every minimal system, netstat -tlnp is the older fallback); or a build failure that scrolled past. To move the port, edit the mapping in compose.yaml.
"каталог data/ належить UID N, а контейнер працюватиме як M"
install.sh caught an ownership mismatch before starting anything — it prints the exact chown to run. The usual cause is a first run under sudo followed by a run without it, or a data/ restored from an archive that carried foreign ownership:
sudo chown -R "$(id -u):$(id -g)" data reports"unable to open database file" in the container log
Same root cause, reached the hard way — the mount isn't writable by the UID in .env. Check that .env matches id -u / id -g, fix ownership as above, then docker compose up -d.
Rootless Docker
Under rootless Docker your UID is remapped inside the container, and INVGUARD_UID from .env may not be what the container actually sees. If the volumes turn out unwritable, the simplest fix is to let the daemon's own mapping own them: remove the user: line from compose.yaml and chown -R 10001:10001 data reports from inside the namespace (podman unshare has an equivalent). This is the one configuration where the .env mechanism doesn't help.
SELinux (Fedora, RHEL, CentOS): Permission denied on the volumes
install.sh detects Enforcing mode and warns you, but it won't edit your compose file. Add :z to both bind mounts in compose.yaml:
volumes:
- ./data:/data:z
- ./reports:/reports:z:z relabels the content as shared. Use :Z (capital) only if this is the sole container touching those directories.
Access
Every request returns 401
The token in your browser is stale — most often after rotate-token or a fresh data/. Read the current one with cat data/.token and re-enter it via the "Токен" button.
data/.token doesn't exist
The container never completed its first start. Check the logs; the token is generated during lifespan startup, right after the database opens.
Lost the token
docker compose exec invguard python -m invguard.cli rotate-token --showFeeds and scanning
The first scan takes forever Expected. The first run pulls the entire CVE corpus — hundreds of thousands of records. Add an NVD API key before you start (⚙ → NVD) for roughly a tenfold speedup. Subsequent runs are incremental and take seconds to minutes.
A source shows up in feeds_failed
By design the run continues on cached data and the report marks that source as stale. Check the reason on the Прогони (Runs) page:
HTTP 403/429from NVD — you're rate-limited. Add an API key."заблоковано httpguard" — the connector tried to reach a domain outside the allowlist, or the target resolved to a private address. For CSAF providers, confirm the URL is an
https://…/provider-metadata.jsonand that its ROLIE feeds live on public hosts.Cisco
401— OAuth2 credentials are wrong or the API subscription lapsed. Re-check client ID and secret at apiconsole.cisco.com.
No findings at all, and that feels wrong Check that your inventory items actually have a CPE (the column is visible in the inventory table). A device with an empty or hand-typed invalid CPE matches nothing. Use "+ Add vendor" first so autocomplete offers dictionary-backed identifiers — everything it suggests is guaranteed to match.
Findings for a version you already patched
Update the version field on the device; the next run closes the finding automatically. If the source's range is genuinely wrong, mark the finding false_positive with a comment — it stays in the history and won't re-alert.
A scan seems stuck
Only one run executes at a time (an in-process lock plus a flock file, so the MCP shim and the web process can't collide). A run triggered while another is in flight waits its turn. If the container was killed mid-run, the kernel releases the flock automatically — just restart.
MCP
The server doesn't appear in Claude Desktop Run the diagnostic first — it reports facts, not guesses:
docker exec -i invguard python -m invguard.cli mcp-check --verboseNo module named 'mcp.server.fastmcp'— an old MCP SDK got baked into a cached layer. Since 1.7.2 the build fails loudly on this instead of shipping a silently broken feature:docker compose build --no-cache && docker compose up -d."MCP: готово" but the client still shows nothing — the config path or the
dockerpath is wrong. Use the absolute binary path; desktop apps don't inherit your shell environment. Then fully quit the client (⌘Q, not just closing the window).Something writes to stdout during import —
mcp-checkcatches this. stdout carries the JSON-RPC protocol; any strayprintbreaks the handshake.
Reading the client's MCP log
The path is OS-specific — setup-mcp.sh prints the right one for your system in its closing hints:
OS | Log |
macOS |
|
Linux |
|
Windows |
|
More detail, in Ukrainian: docs/MCP_TROUBLESHOOTING_UA.md.
UI
The interface looks like the old version after an update
Hard-refresh once: Ctrl+F5 / Cmd+Shift+R. JS modules are served with a ?v=<version> query and the HTML is no-store, but a browser cache can still hold the previous assets for a moment.
Autocomplete offers nothing The knowledge base for that vendor hasn't been built yet. ⚙ → "+ Add vendor", then wait for the background job. Progress and any errors show up in the same modal.
Data
The database is much larger than expected
The CVE corpus dominates. invguard status shows the split. VACUUM happens implicitly during backup, so restoring a backup gives you a compacted file.
Restoring while the service is running
Don't. docker compose down, restore, then up. restore without --force refuses to overwrite an existing database precisely to make this hard to get wrong.
Development
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest # 151 test functions across 22 files
INVGUARD_DATA_DIR=./data uvicorn invguard.main:app --port 8480bash tests/dryrun_install.sh # install.sh on mocked docker/curl — no Docker neededThat harness exists because every installation bug we shipped was platform-specific, and almost all of them were invisible on macOS: Docker Desktop's ownership rewriting hides the entire class of "the container can't write to the bind mount". The mock deliberately emulates native Linux UID semantics and covers a missing curl, a Docker socket without permission, a stopped daemon, inherited root ownership, NO_COLOR, and a re-run for idempotency. pytest invokes it too.
The Python suite runs entirely on recorded feed fixtures — no network, no live API keys. It includes an end-to-end smoke test, a real MCP handshake, guard tests proving that a disabled connector cannot open a connection and that a request leaking a hostname or IP is rejected by the transport, and a version-comparison edge-case matrix.
House rules, learned the hard way:
Routers and MCP tools stay thin — zero logic, calls into the domain only. Two bridges, one core.
Every fix ships with a regression test that fails without it.
innerHTMLis permitted only viaDOMPurify.sanitize(marked.parse(...)), and only for completed markdown reports. Everything else istextContent.Connectors never write to the database. They return normalized records;
store.pyapplies them atomically. That's what makes them pure functions of(client, cursor)and testable without a database.User-facing errors are written in plain Ukrainian, at the point where the domain knows what actually went wrong.
Project layout
invguard/
├── main.py FastAPI app, lifespan, token middleware
├── cli.py maintenance commands
├── mcp_server.py FastMCP tools (thin, like the routers)
├── mcp_stdio.py stdio shim for desktop clients
├── core/
│ ├── config.py pydantic-settings; secrets live in kv, not here
│ ├── db.py SQLite WAL 0600, self-migrating by user_version
│ ├── auth.py bearer token (sha256 in DB), rotation
│ └── httpguard.py allowlist, private-range block, leak filter
├── inventory/ CRUD, CSV import with partial success, CPE catalog
├── feeds/ connector contract + registry
│ └── premium/ optional, dark until keyed
├── matching/ engine (diff vs previous run) + version schemes
├── compliance/ scanner, reporter, alerts, scheduler
├── kb/ knowledge base: worker, sources, autocomplete store
└── routers/ REST, one module per resource
web/ index.html + ES modules + vendored libraries
tests/ pytest suite on recorded fixtures
docs/ architecture, operations, MCP diagnostics (Ukrainian)
scripts/setup-mcp.sh Claude Desktop wiring
install.sh, update.sh first install; zip-based update with rollback
compose.yaml hardened single service
.env.example template for the UID/GID file install.sh generates
tests/dryrun_install.sh install.sh on mocked docker/curl (native-Linux semantics)Runtime state (data/, reports/, backups/, upd/) is created at install time and gitignored. It never lives in the repository.
Container hardening
Not decorative — this is the deployment contract:
ports: ["127.0.0.1:8480:8480"] # loopback only
read_only: true # immutable rootfs, /tmp is tmpfs
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
mem_limit: 1g
memswap_limit: 1g # swap off - fail predictably, don't thrash
user: "${INVGUARD_UID:-10001}:..." # non-root, host UID from .envThe process runs a single uvicorn worker. Large corpora (NVD, CVE List V5) are streamed, never loaded into memory whole.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
AlicenseBqualityDmaintenanceAllows developers to query security findings (SAST issues, secrets, patches) using natural language within AI-assisted tools like Claude Desktop, Cursor, and other MCP-compatible environments.Last updated179MIT- Alicense-qualityBmaintenanceIntegrates 15+ static application security testing tools (Semgrep, Bandit, TruffleHog, etc.) with Claude Code AI, enabling automated vulnerability scanning and security analysis through natural language commands. Supports cross-platform operation with remote execution on dedicated security VMs.Last updated7MIT
- FlicenseBqualityFmaintenanceEnables AI assistants to manage StashDog inventory through natural language commands, supporting item management, collections, tags, smart search, and URL imports with secure authentication.Last updated11
- FlicenseAqualityDmaintenanceExposes Burp Suite's REST API to AI assistants, enabling users to trigger vulnerability scans, monitor progress, and manage security tasks through natural language. It also provides programmatic access to Burp's security knowledge base for querying vulnerability definitions and remediation advice.Last updated81
Related MCP Connectors
CVE lookups (NVD) and dependency-manifest audits (OSV) for AI agents. No API keys.
CVE lookups (NVD) and dependency-manifest audits (OSV) for AI agents. No API keys.
Pay-per-call cybersecurity for AI agents: vuln scans, threat intel, compliance, code security.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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/xordej/invguard'
If you have feedback or need assistance with the MCP directory API, please join our Discord server