Skip to main content
Glama
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).