Manifest
Serve a JSON manifest over HTTPS. pup validates it againstextensionManifestSchema
in packages/extensions/src/manifest.ts and rejects unknown fields.
Signed requests
pup signs every MCP, challenge and event POST with Standard Webhooks:webhook-id,
webhook-timestamp and webhook-signature. Verify the signature over the exact body
bytes, accept a five-minute window, reject replayed MCP and challenge IDs, and accept
either secret during the ten-minute rotation overlap. The challenge endpoint echoes the
signed JSON challenge string.
pup requires HTTPS without URL credentials, pins public IP addresses and rejects
redirects and compressed responses. Each request has a five-second deadline and a
1 MiB response cap; long work returns a Task promptly.
Invocation context
MCP is stateless2026-07-28 over Streamable HTTP. Each request carries
params._meta["io.puphq/context"] with installationId, askerId, channelId
(absent during tool discovery) and settings. Trust it only after verifying the
signature. Recorded calls and their task requests also include callId; discovery
does not. A tools/call for a tool declaring userRequest: true additionally
includes userRequest: { messageId, channelId, content } when it originated from
an assistant conversation. Its author is the verified askerId; its channel is
the original channel, which can differ from the response thread’s channelId.
This snapshot is independent of tool arguments. It does not grant more authority
than the member’s actual request. Other tools receive no source message. pup sends
no transcript, role list or Discord credential.
Tasks
Long tools return an MCP Task (the official Tasks extension). Supporttasks/get and
tasks/update; pup doesn’t cancel tasks. For input, report input_required with a
form elicitation; pup sends the asker’s answer with tasks/update. A changed
statusMessage on a working task resumes the saved conversation with progress
data. Repeated identical status text does not create another update.
Events
Declareevents and endpoints.events. pup sends member.joined and member.left
with the member’s ID. The signed envelope has version, id (equal to webhook-id
and stable across retries), eventId, occurredAt, event and context. Return
{} or { "presentation": ... }. Commit effects once per delivery ID: retries return
the same answer. pup retries from a durable outbox.
Prefer Build an extension when using the SDK; it handles this protocol for you.