Skip to main content
Glama
vinkurov
by vinkurov
README.md
# hookshelf-mcp

**Give your coding agent a real webhook endpoint.** An MCP server that lets Claude Code, Cursor and any MCP client receive webhooks, inspect the exact bytes that arrived, send correctly signed test events for 7 providers, and replay any delivery — backed by a local [hookshelf](https://github.com/vinkurov/hookshelf) instance, so payloads never leave your machine.

[![CI](https://github.com/vinkurov/hookshelf-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/vinkurov/hookshelf-mcp/actions/workflows/ci.yml)
[![license](https://img.shields.io/github/license/vinkurov/hookshelf-mcp)](./LICENSE)

## The problem

Ask an agent to "integrate Stripe webhooks" and it writes the handler blind. It cannot receive a delivery, so it cannot see what Stripe actually sends, cannot check its signature verification against real bytes, and cannot find out whether its fix worked. The usual answer — a public tunnel and clicking around a provider dashboard — is exactly the part an agent cannot do.

With this server the agent closes the loop itself:

```
agent: create_endpoint(name: "stripe-dev", provider: "stripe", secret: "whsec_...")
  →  http://127.0.0.1:3000/in/f4080sjvz3v6tfd5

agent: send_test_event(endpoint_id: "f4080...")        # signed like the real thing
  →  { received: true, delivery: "a698af65..." }

agent: get_delivery(delivery_id: "a698af65...")
  →  headers as received, exact body, verification: "ok"

agent: send_test_event(endpoint_id: "f4080...", tamper: true)
  →  { error: "invalid_signature", delivery: "eb7c9d8e..." }   # failure path, also stored
```

Write handler → send signed event → read what arrived → fix → replay. No third-party service, no tunnel, no dashboard.

## Tools

| Tool | What it does |
| --- | --- |
| `create_endpoint` | New endpoint with its inbound URL. Optional provider+secret for signature verification, optional forward URL. |
| `send_test_event` | Sends a webhook **with a valid signature** for the endpoint's provider: `github`, `stripe`, `slack`, `shopify`, `standard-webhooks`, `paddle`, `telegram`. `tamper: true` breaks the signature on purpose to test the failure path. Fixed `event_id` tests deduplication. |
| `wait_for_delivery` | Blocks until a new delivery arrives — "trigger, wait, inspect" without a polling loop. |
| `get_delivery` | One delivery whole: headers as received, exact body (UTF-8 or base64), verification result, forwarding attempts. |
| `list_deliveries` / `list_endpoints` / `delete_endpoint` | What they say. |
| `replay_delivery` | Queues a stored delivery again, byte for byte, pointing back at the original. |

Twilio is verify-only: it signs the public request URL rather than the body, so only the real provider can produce a valid signature.

## Setup

Two pieces: hookshelf (holds the deliveries) and this server (gives the agent hands).

```bash
# 1. hookshelf
git clone https://github.com/vinkurov/hookshelf.git && cd hookshelf
docker compose up -d        # dashboard on http://127.0.0.1:3000

# 2. this server
git clone https://github.com/vinkurov/hookshelf-mcp.git && cd hookshelf-mcp
npm install && npm run build
```

**Claude Code** — `.mcp.json` in your project (or `claude mcp add`):

```json
{
  "mcpServers": {
    "hookshelf": {
      "command": "node",
      "args": ["/path/to/hookshelf-mcp/dist/main.js"],
      "env": { "HOOKSHELF_URL": "http://127.0.0.1:3000" }
    }
  }
}
```

Cursor and Claude Desktop take the same `command`/`args`/`env` block in their MCP settings. `HOOKSHELF_URL` defaults to `http://127.0.0.1:3000`.

Not on npm yet — `npx hookshelf-mcp` will work once it is published; this README will say so when it does, rather than before.

## Notes worth knowing

- **Signatures are generated from the same specifications webhook-kit verifies against**, and every one is round-trip tested through webhook-kit's actual verifier — generation and verification can only drift if the tests break.
- **Secrets are held in memory only.** hookshelf stores secrets write-only, so `send_test_event` works for endpoints created in the current session; for anything else the server says so instead of guessing.
- **A rejected delivery is still stored.** That is hookshelf's defining behaviour: you cannot debug a request you threw away. The tool returns the delivery id either way, and the agent can inspect exactly what failed.
- **Timestamped schemes sign with unix seconds**, not milliseconds — a millisecond timestamp produces a "valid" signature that fails the freshness check, which is the kind of bug this package exists to catch.
- No auth on hookshelf: keep it bound to loopback (its compose file already does).

## Development

```bash
npm test              # 37 tests: every signature round-trips through webhook-kit's verifier
npm run test:e2e      # 11 checks against a real hookshelf instance
npm run lint && npm run typecheck
```

The unit tests drive the server through a real MCP client over an in-memory transport, against a fake hookshelf whose responses are copied from the real handlers — and the e2e run then checks the copies against reality. It has already caught one drift: the fake deduplicated deliveries on capture-only endpoints, the real hookshelf only deduplicates when forwarding (there is nothing downstream to protect otherwise).

## License

MIT — see [LICENSE](./LICENSE).