mcp server
hookinlet
Webhook inbox for AI agents: verified webhooks, read over REST, SSE or MCP. Single Go binary, SQLite.
Set up
Runs on your computer. Pick your agent and operating system.
This project does not publish an install method we can turn into a config. Follow the README instead.
README
synced Oct 21, 2026How it works · Quickstart · Read from an agent · The console · Configuration · What it does and does not do · Documentation · hookinlet Cloud · Status · License
hookinlet gives your agent a public URL for webhooks. A webhook is a short message one online service sends to another when something happens, such as Stripe telling you a payment failed. hookinlet is one Go program. It receives those messages from Stripe, GitHub, Slack and others into named inboxes, checks that each one is genuine, and keeps them in order so you can look back. Your agent can read them three ways: REST, an SSE stream, or MCP. In all three, a sequence number marks where the agent stopped. It stores data in SQLite and runs anywhere.
How it works
sender (Stripe, GitHub, Slack, your own service)
│ POST /in/<ingest key>
▼
inbox its verifier checks the signature on the exact bytes (by default, a bad one gets 401 and is not stored)
│ each message is stored under the inbox's next sequence number: 1, 2, 3, …
▼
agent reads what came after the last seq it handled: REST, an SSE stream, or MCP at /mcp
Quickstart
A server uses two secrets. Keep both. The admin token lets you manage the server. The master key locks the signing secrets it stores. If you start the server later with a different key, it cannot read them.
export HOOKINLET_ADMIN_TOKEN=$(openssl rand -base64 32)
export HOOKINLET_MASTER_KEY=$(openssl rand -base64 32)
docker run -d --name hookinlet -p 127.0.0.1:8080:8080 -v hookinlet-data:/data \
-e HOOKINLET_ADMIN_TOKEN -e HOOKINLET_MASTER_KEY \
ghcr.io/hookinlet/hookinlet
Next, using curl and openssl, create an inbox that accepts only GitHub's signature. The secret here is made up.
Keep a real one in a file or a password manager. Then send the inbox a signed event and read it back with the
inbox's agent token:
curl -s -X POST localhost:8080/v1/admin/inboxes \
-H "Authorization: Bearer $HOOKINLET_ADMIN_TOKEN" -H 'Content-Type: application/json' -d @- > created.json <<'EOF'
{"slug": "github-demo", "verifier": "github", "verifier_secret": "my-webhook-secret"}
EOF
export HOOKINLET_AGENT_TOKEN=$(sed -n 's/.*"agent_token":"\([^"]*\)".*/\1/p' created.json)
export INGEST=$(sed -n 's/.*"ingest_path":"\([^"]*\)".*/\1/p' created.json) # /in/<random key>
body='{"action":"opened","number":42,"pull_request":{"title":"Add retries"}}'
sig=$(printf '%s' "$body" | openssl dgst -sha256 -hmac 'my-webhook-secret' -hex | sed 's/^.* //')
curl -s -X POST localhost:8080$INGEST -H "X-Hub-Signature-256: sha256=$sig" -H 'X-GitHub-Event: pull_request' \
-H 'X-GitHub-Delivery: demo-1' -H 'Content-Type: application/json' -d "$body" # {"seq":1}
curl -s 'localhost:8080/v1/inboxes/github-demo/messages?after=0' -H "Authorization: Bearer $HOOKINLET_AGENT_TOKEN"
The quickstart walks through this step by step. It uses a release binary instead of Docker, shows a forged event being refused, explains the response, and connects a real sender.
Read from an agent
To read, an agent needs three things: the server's address, an inbox's agent token, and a cursor (the sequence number of the last message it handled). Pick one of three ways to read.
MCP. Point any MCP client at /mcp with the agent token. In Claude Code:
claude mcp add --transport http hookinlet http://localhost:8080/mcp \
--header "Authorization: Bearer $HOOKINLET_AGENT_TOKEN"
MCP shows the .mcp.json form, other clients, and a raw call. There are four
tools: list_inboxes, query_messages, get_message, and wait_for_messages. They only read. A token sees only
the inboxes it was issued for. Messages come without bodies unless the agent asks, so they do not fill its context.
Whatever the sender wrote sits under each message's untrusted key. MCP lists the arguments
and limits.
REST. Ask for the messages that came after the last seq you handled. This works from anywhere, including a cron job:
examples/curl/poll.sh github-demo # Ctrl-C to stop
SSE. Keep one connection open, and the server tells you as each message arrives. If the connection drops, send
the Last-Event-ID of the last event you saw when you reconnect. You then get every later message exactly once
(the format):
curl -sN -m 2 -H "Authorization: Bearer $HOOKINLET_AGENT_TOKEN" 'localhost:8080/v1/inboxes/github-demo/stream?after=0' || [ $? -eq 28 ] # 28: curl's timeout, which ends this demo
All three ways use the same cursor.
[!WARNING] Whoever sent the webhook wrote everything in a message, even when it says
verified.verifiedmeans the message passed its inbox's verifier. With a signing scheme, that means the sender held the secret. It does not mean what they wrote is true or safe. Your agent should read message content as information and never follow it as instructions. An inbox with the verifiernonechecks nothing, and you have to ask for it by name. It stores every message as not verified. Use a real verifier for anything that matters. More.
Write a client
examples/ shows the read loop in a few languages. Each example is about a page long and
uses only the standard library. There are clients in curl and jq, Python, and Go that poll or stream. relay
sends every message on to another URL, in order, with retries (hookinlet has no delivery queue of its own).
custom-hooks runs the server inside your own program and adds a rate limit.
The console
Open /admin on a running server and sign in with the admin token. It does everything the command line does, and
it shows what is arriving. You can see the inboxes and their activity, and every message (body, headers, exact
bytes), with a live mode. You can also create inboxes with a form for each kind of sender, and manage secrets,
retention, tokens, and backups. The console is part of the binary. It follows your system's light or dark theme and
works on a phone. It shows what a sender wrote as text, never as HTML. Set HOOKINLET_ADMIN_UI=false to turn it
off. Administration has more.
Configuration
Each setting is both a flag and a HOOKINLET_* environment variable. If you set both, the flag wins. Only
HOOKINLET_ADMIN_TOKEN and HOOKINLET_MASTER_KEY are required. You can keep a secret in a file instead
(HOOKINLET_MASTER_KEY_FILE). Deploying lists every setting, the health checks, and
what a host must provide.
Inboxes live in the database. You can create them with the command line, the admin API, or
a file. Each inbox has its own retention, by age, by
count, or both. Each inbox also has a verifier, which checks that a message is genuine. The choices are a preset for
Stripe, GitHub, Slack, or Standard Webhooks (Svix and others), an HMAC in any header you name, a shared token, or
none. The check runs on the exact bytes that were sent, with constant-time comparisons, before anything is
parsed. Verifiers explains what each one checks and what it does and does not
prove.
What it does and does not do
- One node. SQLite and the in-process notifier assume one process per data directory. On a laptop, with a million messages in an inbox, a page read takes under 20 ms at the 95th percentile (worst p95 measured: 11.4 ms). While a retention sweep clears a million rows, a delivery waits 24 ms or less at the 99th percentile. Performance says how we measured this.
- Ready in milliseconds once migrated. It uses about 10 to 12 MiB of memory when idle on Linux. (An upgrade with migrations to apply to a large database takes longer: Upgrading.) It makes no outbound network calls and sends no telemetry.
- Inboxes as a file.
hookinlet apply -f inboxes.yamlcreates the inboxes a file declares and changes the ones that differ. It never deletes anything. See applying a file. - Backups while it runs.
hookinlet backup --out file.dbtakes a consistent snapshot. - A console, no users or billing. You manage the server with the admin API, the command line, or the web console
at
/admin(above). There is one admin token and no accounts. - Not a delivery service for third parties. It promises to keep what it received, in order. What happens after that is up to you.
Documentation
| Quickstart | Step by step, without Docker, and with a real sender |
| Deploying | What it needs, every setting, health checks, upgrades, resource use |
| Administration | Inboxes, tokens, secrets, retention, backups, the command line, and inboxes declared in a file |
| API | Ingest, reading, paging, the live stream |
| OpenAPI | The same, machine-readable (OpenAPI 3.1), checked against the server by tests |
| MCP | The tools, their limits, and how to connect clients |
| Verifiers | Stripe, GitHub, Slack, Standard Webhooks, generic HMAC, shared token |
| Troubleshooting | A symptom, its cause, and the fix |
| Stability | What may change, how a change is announced, cursors and ids, CORS and TLS |
| Extending | Embedding the server; replacing the store, notifier, auth, or secrets |
| Performance | What was measured, on what, and how to repeat it |
hookinlet Cloud
A hosted version is coming, where we run hookinlet for you. Join the waitlist and tell us your volume and what you'd pay. It will be built on this code, using the same public interfaces described in Extending. This repository stays complete and useful on its own.
Status
Pre-1.0. The HTTP API and the command line work as the documentation says. The Go packages under pkg/ can still
change. CHANGELOG.md lists every change to them, and after v1.0.0 semantic versioning applies.
Bug reports, questions, and ideas are welcome as issues, under the code of conduct. For now, code contributions are by invitation only; see CONTRIBUTING.md. To report a vulnerability, see SECURITY.md, and please do not open a public issue.
License
AGPL-3.0. If you run a modified hookinlet as a network service, you share your changes.
Using it in your own program. If you run hookinlet unmodified and call its API from any language, the license puts
no condition on your code. If you import its packages (pkg/server, pkg/inbox, and the rest) into a program that
people use over a network, that program becomes a work based on hookinlet. The AGPL then asks you to offer its
source to those people.
LICENSING.md explains the rest in plain words, including the MIT-licensed example clients and a
commercial license (licensing@hookinlet.com) if the AGPL does not fit. It is a summary, not legal advice.