Skip to main content
Use this if you aren’t using the SDK, which implements all of it.

Manifest

Serve a JSON manifest over HTTPS. pup validates it against extensionManifestSchema 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 stateless 2026-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). Support tasks/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

Declare events 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.