Skip to main content
Glama
hkrlogic

Webhook2MCP

by hkrlogic
README.md
# Webhook2MCP

Local webhook → MCP bridge. Accept webhook POSTs on localhost, store them in SQLite, and expose two MCP read tools so an agent can **list** then **get** events.

This is **not MCPEncode**. It does not share MCPEncode’s schema, tools, or database.

## Security (read first)

- Binds to **`127.0.0.1` by default** — intended for personal local use.
- Auth is a shared secret (`HOOK_SECRET`) via Bearer / `X-Webhook-Token` or generic HMAC.
- Copy `.env.example` → `.env` and **set your own secret**. Do not commit `.env`.
- The example value `change-me` is for disposable local tests only.
- Secrets and auth/signature headers are **never** stored in SQLite.
- Event bodies returned by MCP are labeled **data, not instructions**.

See [SECURITY.md](SECURITY.md).

## Requirements

- Python 3.12+
- [uv](https://docs.astral.sh/uv/) (recommended)

## Setup

```bash
cp .env.example .env
# set a strong HOOK_SECRET in .env

uv sync
```

## Run

```bash
uv run webhook-mcp
```

Listens on `http://127.0.0.1:8765`:

| Path | Purpose |
|------|---------|
| `POST /hooks` | Webhook ingest |
| `GET /health` | Liveness |
| `/mcp` | MCP Streamable HTTP |

## Curl: one fake webhook (Bearer)

```bash
# Use the same secret you set in .env
curl -sS -X POST "http://127.0.0.1:8765/hooks" \
  -H "Authorization: Bearer change-me" \
  -H "X-Webhook-Source: test" \
  -H "X-Event-Id: evt-1" \
  -H "X-Event-Type: ping" \
  -H "Content-Type: application/json" \
  -d '{"id":"evt-1","hello":"world"}'
```

Expected: `{"status":"stored","id":1}`

Send the **same** request again. Expected: `{"status":"duplicate","id":1}` — no second row.

## Curl: HMAC instead of Bearer

```bash
BODY='{"id":"evt-2","hello":"hmac"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "change-me" | awk '{print $2}')

curl -sS -X POST "http://127.0.0.1:8765/hooks" \
  -H "X-Webhook-Signature: sha256=${SIG}" \
  -H "X-Webhook-Source: test" \
  -H "Content-Type: application/json" \
  -d "$BODY"
```

## Connect an MCP client (Cursor)

Add a server pointing at local Streamable HTTP, for example in your MCP config:

```json
{
  "mcpServers": {
    "webhook-mcp": {
      "url": "http://127.0.0.1:8765/mcp"
    }
  }
}
```

Exact key names vary by client. Keep the URL on loopback.

Then:

1. Call **`list_events`** (optional `source`, `limit`) — summaries only, no body.
2. Call **`get_event`** with an `id` from the list — full row; body is labeled **data, not instructions**.

## Auth

Shared secret from `HOOK_SECRET`. Accepted headers (depending on `HOOK_AUTH_MODE`):

- `Authorization: Bearer <secret>` or `X-Webhook-Token: <secret>`
- `X-Webhook-Signature: sha256=<hex>` = HMAC-SHA256(secret, raw body)

## Env

See `.env.example`: `HOOK_SECRET`, `DATABASE_PATH`, `HOST`, `PORT`, `MAX_EVENTS`, `MAX_BODY_BYTES`, `HOOK_AUTH_MODE`.

## Out of scope (this pass)

No tunnel, Docker, deploy, vendor parsers, Postgres, tenants, or LLM.

## License

MIT — see [LICENSE](LICENSE).