VAmPI-MCP
by mcropsey
README.md
# VAmPI-MCP — the Vulnerable API, over MCP
A faithful port of [erev0s/VAmPI](https://github.com/erev0s/VAmPI) from a REST /
OpenAPI service to a **Model Context Protocol (MCP) server**. There is no HTTP
API and no Swagger spec anymore — every former endpoint is now an **MCP tool**.
The vulnerabilities live in the handler logic and the data model, so they port
over unchanged and are reachable by any MCP client (Claude Desktop, opencode,
an agent, a scanner, etc.).
> ⚠️ **Vulnerable on purpose.** Run it locally / in an isolated lab only. Do not
> expose it to untrusted networks. It stores plaintext passwords, is injectable,
> and hands out admin on request — by design.
---
## What changed vs. upstream VAmPI
| Upstream (REST) | This port (MCP tool) |
| ----------------------------------- | ------------------------- |
| `GET /` | `api_info` |
| `GET /createdb` | `create_db` |
| `GET /users/v1` | `get_all_users` |
| `GET /users/v1/_debug` | `debug_users` |
| `POST /users/v1/register` | `register_user` |
| `POST /users/v1/login` | `login` |
| `GET /me` | `me` |
| `GET /users/v1/{username}` | `get_user_by_name` |
| `PUT /users/v1/{username}/email` | `update_email` |
| `PUT /users/v1/{username}/password` | `update_password` |
| `DELETE /users/v1/{username}` | `delete_user` |
| `GET /books/v1` | `get_all_books` |
| `POST /books/v1` | `add_book` |
| `GET /books/v1/{book_title}` | `get_book_by_title` |
**Auth model.** MCP has no request headers, so the old
`Authorization: Bearer <jwt>` becomes an explicit **`token` argument** on every
authenticated tool. Log in with `login`, then pass the returned `auth_token`
into `me`, `update_email`, `update_password`, `delete_user`, `add_book` and
`get_book_by_title`.
### Vulnerabilities preserved (with `vulnerable=1`)
- **Excessive data exposure** — `debug_users` leaks passwords + admin flags (leaks even when hardened).
- **Mass assignment** — `register_user(..., admin=true)` grants yourself admin.
- **SQL injection** — `get_user_by_name` concatenates the username into raw SQL.
- **User / password enumeration** — `login` returns distinct error messages.
- **BOLA (object-level auth)** — `update_password` changes *any* user's password; `get_book_by_title` reads *any* user's secret.
- **Broken JWT auth** — HS256 signed with the hard-coded key `random`.
- **ReDoS** — `update_email` uses a catastrophic-backtracking regex.
Set `vulnerable=0` to serve the hardened variant (parameterised queries,
ownership checks, generic login errors). `debug_users` stays leaky regardless —
that's bad practice, not a toggle.
### Dropped (transport-specific, no MCP equivalent)
HTTP verb tampering, CORS misconfig, rate limiting, and OpenAPI
spec-conformance testing don't exist in an MCP transport, so they're gone.
---
## Seeded data
`create_db` (and first launch) seeds three users:
| username | password | admin |
| -------- | -------- | ----- |
| name1 | pass1 | no |
| name2 | pass2 | no |
| admin | pass1 | yes |
Each user also gets one book with secret content.
---
## Requirements
- **Python 3.10+** (this repo was verified on 3.12).
- The macOS system `python3` may be 3.9 — too old. Use Homebrew Python:
`brew install python@3.12`.
---
## Local install & run
```bash
cd ~/Downloads/vampi-mcp
# use a 3.10+ interpreter
python3.12 -m venv .venv
.venv/bin/pip install --upgrade pip
.venv/bin/pip install -e . # installs deps + the `vampi-mcp` command
```
Run it (stdio — the transport local clients use):
```bash
.venv/bin/vampi-mcp
# equivalent: .venv/bin/python -m vampi_mcp
```
It will create and seed `vampi_mcp/database.db` on first launch. A stdio server
has no interactive output — it waits for an MCP client to speak to it over
stdin/stdout. To point a client at it, use the absolute path
`~/Downloads/vampi-mcp/.venv/bin/vampi-mcp` (see the client sections below).
### Configuration (env vars)
| Variable | Default | Meaning |
| ----------------- | --------- | -------------------------------------------------- |
| `vulnerable` | `1` | `1` vulnerable, `0` hardened |
| `tokentimetolive` | `60` | JWT lifetime in seconds |
| `MCP_TRANSPORT` | `stdio` | `stdio` or `streamable-http` |
| `MCP_HOST` | `0.0.0.0` | bind host (HTTP transport) |
| `MCP_PORT` | `5000` | bind port (HTTP transport) |
| `VAMPI_DB_PATH` | in-package | path to the sqlite file |
### Smoke test
```bash
.venv/bin/python - <<'PY'
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
p = StdioServerParameters(command=".venv/bin/vampi-mcp", args=[])
async with stdio_client(p) as (r,w):
async with ClientSession(r,w) as s:
await s.initialize()
print([t.name for t in (await s.list_tools()).tools])
asyncio.run(main())
PY
```
---
## Docker (networked / streamable-HTTP transport)
The container runs the server over **streamable-HTTP** so it's reachable on the
network. The MCP endpoint is `http://<host>:5000/mcp/` (note the trailing slash).
### docker compose (recommended)
```bash
cd ~/Downloads/vampi-mcp
docker compose up --build -d
docker compose logs -f # watch it start
docker compose down # stop
```
### plain docker
```bash
cd ~/Downloads/vampi-mcp
docker build -t vampi-mcp:latest .
docker run -d --name vampi-mcp -p 5000:5000 \
-e vulnerable=1 -e tokentimetolive=60 \
-v vampi-data:/data vampi-mcp:latest
```
Verify it's up:
```bash
curl -s -X POST http://localhost:5000/mcp/ \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}'
```
You should get an SSE `event: message` line with a JSON-RPC `result`.
---
## Add it to opencode
opencode reads `~/.config/opencode/opencode.json` (global) or an
`opencode.json` in your project root. Add an `mcp` entry.
### Local (stdio) — no Docker needed
```json
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"vampi": {
"type": "local",
"command": [
"/Users/mcropsey/Downloads/vampi-mcp/.venv/bin/vampi-mcp"
],
"enabled": true,
"environment": {
"vulnerable": "1",
"tokentimetolive": "60"
}
}
}
}
```
### Remote (the Docker container over HTTP)
```json
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"vampi-http": {
"type": "remote",
"url": "http://localhost:5000/mcp/",
"enabled": true
}
}
}
```
Restart opencode. Ask it to "list VAmPI tools" or "call api_info" to confirm.
---
## Add it to Claude Desktop
Claude Desktop launches local MCP servers over **stdio**. Edit its config file:
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
Add (or merge) an `mcpServers` entry. Use the **absolute path** to the console
script inside the venv:
```json
{
"mcpServers": {
"vampi-mcp": {
"command": "/Users/mcropsey/Downloads/vampi-mcp/.venv/bin/vampi-mcp",
"args": [],
"env": {
"vulnerable": "1",
"tokentimetolive": "60"
}
}
}
}
```
Then fully **quit and reopen Claude Desktop**. The VAmPI tools appear under the
tools (🔌 / hammer) menu. Try: *"Use api_info, then log in as admin/pass1 and
call debug_users."*
> Claude Desktop connects only over stdio, so it uses the local venv command,
> not the Docker HTTP endpoint. Run the Docker version for clients that support
> remote/streamable-HTTP MCP (like opencode's `remote` type).
---
## Test prompts
See [TEST_PROMPTS.md](TEST_PROMPTS.md) for ready-to-paste natural-language
prompts (for Claude Desktop / any MCP client) that exercise every tool — a
happy-path walkthrough, a per-endpoint reference mapped to the old REST calls,
and the full set of vulnerability exercises.
## Example attack walk-through (any client)
1. `api_info` → confirms `vulnerable: 1`.
2. `debug_users` → dumps all passwords (excessive data exposure).
3. `register_user(username="evil", password="pw", email="e@x.com", admin=true)` → mass assignment.
4. `login(username="evil", password="pw")` → grab `auth_token`.
5. `get_user_by_name(username="name1' OR '1'='1")` → SQL injection.
6. `update_password(token=<evil token>, username="admin", password="pwned")` → BOLA: hijack admin.
7. `get_book_by_title(token=<evil token>, book_title="<someone else's book>")` → BOLA: read another user's secret.
---
## Project layout
```
vampi-mcp/
├── vampi_mcp/
│ ├── __init__.py
│ ├── __main__.py # python -m vampi_mcp
│ ├── config.py # env-driven config (vulnerable, TTL, transport)
│ ├── models.py # User/Book models + sqlite + SQLi/JWT logic
│ └── server.py # FastMCP server: all 14 tools
├── requirements.txt
├── pyproject.toml # installs the `vampi-mcp` command
├── Dockerfile
├── docker-compose.yaml
├── TEST_PROMPTS.md # ready-to-paste prompts for every tool
└── README.md
```
Credit: original VAmPI by [erev0s](https://github.com/erev0s/VAmPI).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues