Skip to content

Commit e70c56e

Browse files
authored
Model Context Protocol (MCP) server (#205)
* Model Context Protocol (MCP) server Signed-off-by: Prabhu Subramanian <prabhu@appthreat.com> * Fix tests Signed-off-by: Prabhu Subramanian <prabhu@appthreat.com> --------- Signed-off-by: Prabhu Subramanian <prabhu@appthreat.com>
1 parent 147392b commit e70c56e

File tree

18 files changed

+1599
-59
lines changed

18 files changed

+1599
-59
lines changed

.github/workflows/pythonpublish.yml

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ name: Upload vdb Python Package
22

33
on:
44
push:
5+
paths-ignore:
6+
- '**/README.md'
57
tags:
68
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
7-
9+
workflow_dispatch:
810

911
jobs:
1012
deploy:
11-
1213
runs-on: ubuntu-latest
1314
permissions:
1415
contents: write
@@ -52,3 +53,38 @@ jobs:
5253
bom.json
5354
env:
5455
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
56+
57+
container:
58+
runs-on: ubuntu-latest
59+
permissions:
60+
contents: write
61+
packages: write
62+
id-token: write
63+
steps:
64+
- uses: actions/checkout@v4
65+
- name: Set up QEMU
66+
uses: docker/setup-qemu-action@v3
67+
- name: Set up Docker Buildx
68+
uses: docker/setup-buildx-action@v3
69+
- name: Log in to the Container registry
70+
uses: docker/login-action@v3
71+
with:
72+
registry: ghcr.io
73+
username: ${{ github.actor }}
74+
password: ${{ secrets.GITHUB_TOKEN }}
75+
- name: Extract metadata (tags, labels) for Docker
76+
id: meta
77+
uses: docker/metadata-action@v5
78+
with:
79+
images: |
80+
ghcr.io/appthreat/mcp-server-vdb
81+
- name: Build and push Docker images
82+
uses: docker/build-push-action@v5
83+
with:
84+
context: contrib/mcp-server-vdb
85+
platforms: linux/amd64,linux/arm64
86+
push: true
87+
tags: ${{ steps.meta.outputs.tags }}
88+
labels: ${{ steps.meta.outputs.labels }}
89+
cache-from: type=gha,scope=mcp-server-vdb
90+
cache-to: type=gha,mode=max,scope=mcp-server-vdb

contrib/claude-context.png

43.4 KB
Loading

contrib/claude-permissions.png

83.7 KB
Loading

contrib/latest-malware.png

281 KB
Loading

contrib/mcp-server-vdb/.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Python-generated files
2+
__pycache__/
3+
*.py[oc]
4+
build/
5+
dist/
6+
wheels/
7+
*.egg-info
8+
9+
# Virtual environments
10+
.venv

contrib/mcp-server-vdb/Dockerfile

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS uv
2+
3+
# Install the project into `/app`
4+
WORKDIR /app
5+
6+
# Enable bytecode compilation
7+
ENV UV_COMPILE_BYTECODE=1
8+
9+
# Copy from the cache instead of linking since it's a mounted volume
10+
ENV UV_LINK_MODE=copy
11+
12+
# Install the project's dependencies using the lockfile and settings
13+
RUN --mount=type=cache,target=/root/.cache/uv \
14+
--mount=type=bind,source=uv.lock,target=uv.lock \
15+
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
16+
uv sync --frozen --no-install-project --no-dev --no-editable
17+
18+
# Then, add the rest of the project source code and install it
19+
# Installing separately from its dependencies allows optimal layer caching
20+
ADD . /app
21+
RUN --mount=type=cache,target=/root/.cache/uv \
22+
uv sync --frozen --no-dev --no-editable
23+
24+
FROM python:3.12-slim-bookworm
25+
26+
LABEL maintainer="Team AppThreat" \
27+
org.opencontainers.image.authors="Team AppThreat <cloud@appthreat.com>" \
28+
org.opencontainers.image.source="https://github.com/AppThreat/vulnerability-db" \
29+
org.opencontainers.image.url="https://github.com/AppThreat/vulnerability-db" \
30+
org.opencontainers.image.version="1.0.0" \
31+
org.opencontainers.image.vendor="appthreat" \
32+
org.opencontainers.image.licenses="MIT" \
33+
org.opencontainers.image.title="vulnerability-db" \
34+
org.opencontainers.image.description="MCP server for AppThreat's vulnerability database and package search library." \
35+
org.opencontainers.docker.cmd="docker run --rm -e VDB_HOME=/db -v /db:/db -v $(pwd):/app:rw -t ghcr.io/appthreat/mcp-server-vdb"
36+
37+
WORKDIR /app
38+
39+
COPY --from=uv /root/.local /root/.local
40+
COPY --from=uv --chown=app:app /app/.venv /app/.venv
41+
42+
# This directory must contain the vulnerability database
43+
# export VDB_HOME=/db
44+
# vdb --download-image
45+
mkdir -p /db
46+
ENV PATH="/app/.venv/bin:$PATH" \
47+
VDB_HOME="/db"
48+
49+
ENTRYPOINT ["mcp-server-vdb"]

contrib/mcp-server-vdb/README.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Introduction
2+
3+
4+
5+
## Local development
6+
7+
```shell
8+
git clone https://github.com/AppThreat/vulnerability-db.git
9+
cd vulnerability-db
10+
python -m pip install .
11+
vdb --download-image
12+
export VDB_HOME=$HOME/vdb
13+
mkdir -p $VDB_HOME
14+
uv --directory contrib/mcp-server-vdb run mcp-server-vdb
15+
```
16+
17+
## Claude Desktop configuration
18+
19+
Edit the file using VS code or any editor of your choice. `~/Library/Application Support/Claude/claude_desktop_config.json`. On Windows, the config file is `$env:AppData\Claude\claude_desktop_config.json`.
20+
21+
Use the below configuration and adjust the following paths:
22+
23+
- absolute path to the `mcp-server-vdb` inside the contrib directory.
24+
- `VDB_HOME` - Full path to the directory containing the vulnerability database. Must have run `vdb --download-image`
25+
26+
```json
27+
{
28+
"mcpServers": {
29+
"vdb": {
30+
"command": "uv",
31+
"args": [
32+
"--directory",
33+
"/Volumes/Work/AppThreat/vulnerability-db/contrib/mcp-server-vdb",
34+
"run",
35+
"mcp-server-vdb"
36+
],
37+
"env": {
38+
"VDB_HOME": "/Users/guest/vdb"
39+
}
40+
}
41+
}
42+
}
43+
```
44+
45+
Restart the Claude Desktop application.
46+
47+
### Claude context screen
48+
49+
![Claude context](../claude-context.png)
50+
51+
### Claude permissions on first run
52+
53+
![Claude permissions](../claude-permissions.png)
54+
55+
### Claude results
56+
57+
![Vulnerability description](../vuln-description.png)
58+
59+
### Latest malware
60+
61+
![Latest Malware](../latest-malware.png)
62+
63+
## Configuration for MCP Inspector
64+
65+
- Transport Type: STDIO
66+
- Command: python
67+
- Arguments: vdb/server.py
68+
69+
Click "Connect"
70+
71+
![MCP Inspector](./docs/vdb-mcp-inspector.png)
72+
73+
### Testing
74+
75+
1. Click "List Tools". Must see a list of tools such as `search_by_purl_like`, `search_by_any`, and so on.
76+
2. Select `search_by_purl_like` and enter a purl string such as `pkg:swift/vapor/vapor@4.89.0`.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
[project]
2+
name = "mcp-server-vdb"
3+
version = "0.1.0"
4+
description = "AppThreat Vulnerability Database MCP server"
5+
authors = [
6+
{name = "Team AppThreat", email = "cloud@appthreat.com"},
7+
]
8+
readme = "README.md"
9+
requires-python = ">=3.10"
10+
license = {text = "MIT"}
11+
classifiers = [
12+
"Development Status :: 5 - Production/Stable",
13+
"Intended Audience :: Developers",
14+
"Intended Audience :: System Administrators",
15+
"License :: OSI Approved :: MIT License",
16+
"Operating System :: OS Independent",
17+
"Programming Language :: Python :: 3.13",
18+
"Programming Language :: Python :: 3.12",
19+
"Programming Language :: Python :: 3.11",
20+
"Programming Language :: Python :: 3.10",
21+
"Topic :: Security",
22+
"Topic :: Utilities",
23+
]
24+
25+
dependencies = [
26+
"appthreat-vulnerability-db[oras]>=6.2.3",
27+
"mcp[cli]>=1.2.1",
28+
]
29+
30+
[build-system]
31+
requires = ["hatchling"]
32+
build-backend = "hatchling.build"
33+
34+
[project.scripts]
35+
mcp-server-vdb = "mcp_server_vdb:main"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from . import server
2+
import asyncio
3+
4+
from contextlib import suppress
5+
6+
7+
def main():
8+
with suppress(asyncio.CancelledError, KeyboardInterrupt):
9+
asyncio.run(server.run())
10+
11+
__all__ = ["main", "server"]
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import base64
2+
3+
from rich.markdown import Markdown
4+
from rich.table import Table
5+
6+
from vdb.lib.cve_model import CVE
7+
8+
9+
def add_table_row(table: Table, res: dict, added_row_keys: dict):
10+
# matched_by is the purl or cpe string
11+
row_key = f"""{res["matched_by"]}|{res.get("source_data_hash")}"""
12+
# Filter duplicate rows from getting printed
13+
if added_row_keys.get(row_key):
14+
return
15+
source_data: CVE = res.get("source_data")
16+
descriptions = []
17+
cna_container = source_data.root.containers.cna
18+
if cna_container and cna_container.descriptions and cna_container.descriptions.root:
19+
for adesc in cna_container.descriptions.root:
20+
description = (
21+
"\n".join(
22+
[
23+
base64.b64decode(sm.value).decode("utf-8")
24+
for sm in adesc.supportingMedia
25+
]
26+
)
27+
if adesc.supportingMedia
28+
else adesc.value
29+
)
30+
description = description.replace("\\n", "\n").replace("\\t", " ")
31+
descriptions.append(description)
32+
table.add_row(
33+
res.get("cve_id"),
34+
res.get("matched_by"),
35+
Markdown("\n".join(descriptions), justify="left", hyperlinks=True),
36+
)
37+
added_row_keys[row_key] = True

0 commit comments

Comments
 (0)