Skip to main content
Glama
jstjep00

dehashed-mcp

by jstjep00
README.md
# dehashed-mcp

An MCP (Model Context Protocol) server that wraps the [DeHashed](https://dehashed.com) breach-data API and exposes every available endpoint as an MCP tool — with **server-side API key management**, **structured Docker logging**, and a **pluggable RBAC/ABAC authorization engine** that enforces access control at the individual tool call level.

---

## Key design decisions

| Concern | Decision |
|---|---|
| **API key** | Held exclusively in the server process (env var). Never passed to or returned by any tool. |
| **Transport** | Not decided here — wire stdio, HTTP/SSE, or a gRPC proxy on top. |
| **MCP auth** | Not decided here — add bearer token / OAuth / mTLS at the transport layer. |
| **Tool-level authz** | RBAC + ABAC via a hot-reloadable YAML policy file. Per-tool, per-tenant, per-domain-scope, per-page-size. |
| **Logging** | structlog → JSON → stdout/stderr → `docker logs` / any log driver. |

---

## Project layout

```
dehashed-mcp/
├── src/dehashed_mcp/
│   ├── __init__.py
│   ├── main.py            # FastMCP app + entrypoint
│   ├── config.py          # Pydantic Settings (reads env vars)
│   ├── client.py          # Async DeHashed HTTP client
│   ├── models.py          # Request/response Pydantic models
│   ├── tools.py           # All 17 MCP tool definitions
│   ├── authz.py           # RBAC/ABAC engine
│   └── logging_config.py  # structlog → Docker logs
├── config/
│   └── policy.yaml        # Default authorization policy
├── tests/
│   ├── conftest.py
│   ├── test_authz.py
│   ├── test_client.py
│   └── test_models.py
├── Dockerfile
├── docker-compose.yml
├── docker-entrypoint.sh
├── pyproject.toml
└── .env.example
```

---

## Quick start

### 1. Configure

```bash
cp .env.example .env
# Edit .env — set DEHASHED_API_KEY at minimum
```

### 2. Build and run (stdio — Claude Desktop / any MCP host)

```bash
docker compose build
docker compose run --rm -i dehashed-mcp
```

### 3. Run with HTTP/SSE transport

```bash
MCP_TRANSPORT=http docker compose up
# Server listens on http://localhost:8000
```

### 4. View logs

```bash
docker logs -f dehashed-mcp
# Output is newline-delimited JSON
```

---

## Environment variables

| Variable | Required | Default | Description |
|---|---|---|---|
| `DEHASHED_API_KEY` | **yes** | — | Your DeHashed API key |
| `DEHASHED_EMAIL` | only for v1 | — | Account email (V1 Basic Auth only) |
| `DEHASHED_AUTH_MODE` | no | `v2_key` | `v2_key` or `v1_basic` |
| `DEHASHED_BASE_URL` | no | `https://api.dehashed.com` | API base URL |
| `LOG_LEVEL` | no | `INFO` | `DEBUG` / `INFO` / `WARNING` / `ERROR` |
| `MCP_TRANSPORT` | no | `stdio` | `stdio` or `http` |
| `PORT` | no | `8000` | HTTP listen port (http transport only) |
| `AUTHZ_POLICY_PATH` | no | `/config/policy.yaml` | Path to policy file |
| `AUTHZ_STRICT_TENANT` | no | `true` | Reject unknown tenants when `true` |

---

## Available tools (17)

| Tool | Description | Tags |
|---|---|---|
| `search` | Raw query string search | read, raw_query |
| `search_by_field` | Field + value + match mode | read |
| `search_email` | Email exact search | read |
| `search_username` | Username contains search | read |
| `search_password` | Plaintext password exact search | read, sensitive |
| `search_hashed_password` | Hash exact search | read, sensitive |
| `search_ip` | IP address exact search | read |
| `search_name` | Name contains search | read |
| `search_vin` | VIN exact search | read |
| `search_address` | Address contains search | read |
| `search_phone` | Phone contains search | read |
| `search_domain` | Domain contains search | read |
| `search_database` | Breach/database name search | read |
| `search_license_plate` | License plate exact search | read |
| `search_crypto_address` | Cryptocurrency address exact search | read |
| `get_balance` | Retrieve remaining API credits | read, account |
| `paginate_all` | Auto-paginate and aggregate | read, bulk, raw_query |

### Query match modes (`search_by_field`, `search`)

| Mode | Syntax | Notes |
|---|---|---|
| `contains` | `field:value` | Default loose match |
| `exact` | `field:"value"` | Quoted exact match |
| `regex` | `field:/pattern/` | ⚠ Reported broken in V2 (May 2025) |
| `wildcard` | `field:"val*ue"` | `?` = single char, `*` = multi ⚠ May be broken in V2 |

---

## Authorization — RBAC/ABAC policy

Tool-level authorization is enforced **before** any DeHashed API call. The policy is a YAML file (`config/policy.yaml`) that can be hot-mounted into the container.

### Policy structure

```yaml
tenants:
  <tenant_id>:
    roles:
      <role_name>:
        allow: [<tool_name>, ...]   # or ["*"] for all tools
        deny:  [<tool_name>, ...]   # always wins over allow
        resource_scopes:
          domains: [acme.com, ...]  # restrict domain/email tools
    attributes:
      max_page_size: 500            # cap the `size` param
      max_pages: 3                  # cap paginate_all max_pages

default:                            # fallback (strict_tenant=false only)
  roles: ...
```

### CallerContext injection

The MCP metadata dict passed with each tool call is the contract between your transport/auth layer and the authz engine:

```python
# What your transport layer injects into _meta:
{
    "tenant": "acme",
    "roles": ["analyst"],
    "attributes": {}   # optional per-call ABAC overrides
}
```

At the transport layer (bearer token, OAuth, mTLS, API key validation), you decode the identity and populate this dict before the tool handler runs. The server enforces it on every call.

### Hot-reload policy

Send `SIGUSR1` or call `authz.get_policy().reload()` to reload the policy file without restarting the server.

---

## Transport configuration

This server is transport-agnostic. Choose your transport at deploy time:

### stdio (default)
Best for Claude Desktop integration:
```json
{
  "mcpServers": {
    "dehashed": {
      "command": "docker",
      "args": ["run", "--rm", "-i",
               "-e", "DEHASHED_API_KEY=xxx",
               "dehashed-mcp:latest"]
    }
  }
}
```

### HTTP / SSE
Run with `MCP_TRANSPORT=http`. The FastMCP app is also exported as an ASGI `app` object for direct uvicorn/gunicorn use:
```bash
uvicorn dehashed_mcp.main:app --host 0.0.0.0 --port 8000
```
Add your auth middleware (bearer token validation, OAuth introspection, etc.) as ASGI middleware on top.

### gRPC
Put a gRPC-to-HTTP/2 transcoding proxy (e.g., Envoy, grpc-gateway) in front of the HTTP transport.

---

## Development

```bash
# Install with dev extras
pip install -e ".[dev,http]"

# Run tests
pytest

# Lint
ruff check src tests
black --check src tests
mypy src
```

---

## Logging

All log output is **newline-delimited JSON** sent to stdout. Docker captures it via the configured log driver (`json-file` by default in `docker-compose.yml`).

```bash
docker logs dehashed-mcp | jq .
```

Example log line:
```json
{
  "event": "dehashed.search.ok",
  "total": 42,
  "returned": 100,
  "balance": 95,
  "service": "dehashed-mcp",
  "version": "0.1.0",
  "level": "info",
  "timestamp": "2025-03-09T12:00:00.000Z"
}
```

API keys, passwords, and tokens are automatically redacted from all log lines by the `_redact_api_key` processor in `logging_config.py`.

---

## Credits and depletion

Each tool call that hits the DeHashed API costs **1 credit**. `paginate_all` costs 1 credit per page. Monitor balance via `get_balance` or by watching the `balance` field in search results logged at `INFO` level. Policy `max_pages` attributes help prevent runaway credit consumption.

---

## License

MIT