hookshelf-mcp
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.
[](https://github.com/vinkurov/hookshelf-mcp/actions/workflows/ci.yml)
[](./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).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues