# mcpmem-webhook
The durable webhook delivery worker of
[mcpmem](https://github.com/abankowski/mcpmem), an MCP server that gives LLM
agents persistent memory.
This crate is a library. It delivers graph change events to an HTTPS endpoint,
at least once, from a SQLite outbox.
```sh
cargo install mcpmem --features webhooks
```
**Read this before you plan a deployment.** The shipped `mcpmem` binary starts
the `webhooks` role with no worker, and the role then fails with
`webhook role selected without configured worker ports`. The failure is
deliberate and visible, instead of a silent no-op. Delivery works for a program
that embeds this crate and constructs `WebhookWorker::new` with a connector, a
secret provider, a hostname allowlist and a resolver. No MCP tool manages a
subscription in 1.0.0; a user writes the `webhook_subscription` table, or calls
`SubscriptionRepository`.
## Subscriptions
One row of `webhook_subscription` is one subscription:
| `endpoint` | The HTTPS URL, 2048 characters maximum |
| `event_operations` | A JSON array of `create`, `update`, `delete`, `rename`. An empty array matches every kind |
| `entity_types` | A JSON array of entity types. An empty array matches every type |
| `consumer_origin` | The origin name of this consumer, 256 characters maximum |
| `ignored_origins` | A JSON array of origins to drop |
| `secret_ref` | The lookup name of the signing key, 512 characters maximum |
| `enabled` | 0 or 1 |
A subscription never receives its own writes: the filter drops an event whose
origin equals `consumer_origin`, or is in `ignored_origins`. This stops a
delivery loop between two instances.
## Delivery
A mutation writes one `event_outbox` row per matching enabled subscription, in
the same transaction as the graph write, so a crash loses no delivery.
One delivery per subscription is in flight, in one process only. `claim_due`
takes a 30-second lease with a token and an epoch, and a write-back succeeds
only while the token, the epoch and the deadline still match. A slow worker
whose lease expired cannot overwrite a newer attempt.
The worker retries a 408, a 429, and a status of 500 or more. Any other non-2xx
status, a policy error and a secret error kill the delivery at once, because a
repeat cannot succeed. The delay is the `Retry-After` header, clamped from one
second to one hour, and one second otherwise. After eight attempts the row
moves to `dead` and keeps `last_error`. A dead row is never claimed again, and
the server raises no alert.
## The request
| `X-Memory-Signature` | The lowercase hexadecimal HMAC-SHA256 of `<timestamp_us>.<body>` |
| `X-Memory-Timestamp` | The signed timestamp, in microseconds |
| `Idempotency-Key` | The event id. Duplicate suppression is the receiver's job |
A receiver must recompute the MAC over both parts to reject a replayed body.
The body is a version 2 JSON envelope, bounded to 64 KiB. It carries
`eventId`, `transactionId`, `entityId`, `entityRevision`, `operation`,
`occurredAtUs`, `origin`, `correlationId`, `causationId`, `hopCount`, and, for a
rename, `oldName` and `newName`. It carries no observation content: the
receiver reads the graph for the entity state.
## The endpoint policy
The URL must use `https` on port 443, with a hostname from the allowlist, no
user name, no password and no fragment. The worker resolves the hostname,
refuses a private, loopback, link-local, multicast or unspecified address, pins
the connection to the resolved address, and disables redirects.
## Secrets
`secret_ref` is a lookup name, not key material. The embedder supplies a
`SecretProvider`; the crate ships `StaticSecretProvider`, a map from name to
key. An unknown name dead-letters the delivery with
`secret reference is not configured`.
## Documentation
The full server documentation is in the
[workspace README](https://github.com/abankowski/mcpmem#readme). The release
notes are in
[CHANGES.md](https://github.com/abankowski/mcpmem/blob/main/CHANGES.md).
## License
Apache-2.0. See
[LICENSE](https://github.com/abankowski/mcpmem/blob/main/LICENSE) and
[NOTICE](https://github.com/abankowski/mcpmem/blob/main/NOTICE), which records
the derivation from `corporatepiyush/mcp-memory` 5.2.1.