Quick start
The SDK and starter packages are not published to npm yet. With private-beta access to the Tricks repository, run an example from source:trick.ts. Use examples/snack-club for a fuller example
with a write tool, a long-running task, a command and buttons. Each project includes
an AGENTS.md written for coding agents.
bun trick.ts needs TRICK_PUBLIC_URL, the HTTPS address Pup reaches it at
(usually a tunnel), and TRICK_WEBHOOK_SECRETS, the signing secret Pup shows when
you register. Then follow the installation guide: register
<TRICK_PUBLIC_URL>/manifest.json, test the connection and enable it.
A trick in one file
/manifest.json,
/challenge, /mcp and /events. It verifies Pup’s signatures, rejects replays,
validates tool input against your zod schema and runs the MCP server. You write
tools; you don’t handle the protocol.
Tools
run({ input, askerId, channelId, installationId, settings })receives validated input. Pup has verified who asked, where, and on which server. Never trust IDs that arrive as tool arguments; scope stored data byinstallationId, and byaskerIdwhen it is personal.- Set
userRequest: trueonly when a tool needs the original request. Its handler then receivesuserRequest: { messageId, channelId, content }from Pup’s signed context, separate from the assistant-writteninput. It is the triggering member’s message recorded for that call, not a transcript. Commands and task polling do not supply it. Later edits cannot change an existing call’s snapshot. callId, when supplied, is Pup’s stable invocation ID. Persist it withinstallationIdto avoid submitting the same remote work again after a restart. It is not an authorization credential.- Return data, not messages. Pup’s model reads the result and replies in its own words. It never names your trick or relays raw text from it.
readOnly: truemarks a tool that changes nothing. Any other tool is a write, and Pup may ask the member to approve it first (see Approvals). UsealwaysAsk: truefor irreversible writes such as deleting or paying.titleappears on Pup’s approval card.descriptiontells the model when to use the tool, so be specific.- Throw for real failures. Pup tells the member something went wrong without showing your error text.
Commands and buttons
A command runs one tool directly, without the model. Like Pup’s own commands, each works as/name and with the server’s prefix, for example ^name. Set
surfaces: ["slash"] or ["prefix"] to offer only one form.
Commands and buttons show the tool’s pup presentation, so add one to any tool they
use:
text, fields, links and up to five actions. Each
action is a button that runs its tool with fixed arguments for whoever clicks it,
after Pup checks their access. A button’s result is private to the clicker.
visibility decides who sees a /name result: "ephemeral" (the default) shows it
only to the person who ran it, "public" to the channel. A prefix command is typed
in the channel, so its reply is always visible there. Command options are flat
string, integer, number, boolean, user, channel or role values that
fill the tool argument of the same name. With the prefix, optional values are named:
^snacks --count=2.
Typing a command or clicking a button is the member’s own decision, like /ban: it
runs without an approval card. Pup’s permissions decide who may run it.
Long-running tasks
run in task() for work that takes a while or needs the member’s input.
await ask(message, schema) pauses the task. Pup’s model asks the question in its own
words; the member answers normally, by replying or with their next message, and the
answer arrives validated. Keep answer schemas flat: strings, numbers, booleans and
enums. Never ask for passwords, tokens or keys.
Pup waits a few seconds for a task in the same turn, so a quick task answers like any
tool. A longer one continues the conversation when it finishes, asks something, or reports new progress.
Tasks belong in conversations; a command or button that returns a task fails
privately. The SDK keeps task state in memory, so a restart ends running tasks and
Pup tells the member it lost track of them.
For work managed by another service, use externalTask({ start, get, cancel? }).
Persist its reference in the trick and return it promptly from start; Pup polls
get after restarts without submitting the work again. A working state can include
progress: "The tests passed; checking the build.". Report meaningful changes,
not every command or unchanged poll. Pup treats progress as untrusted data and
summarizes it in its own voice. Never include secrets or raw command output.
Set thread: true on the tool to keep an assistant request and its updates in a
Discord thread. Pup creates one from the triggering message or reuses the current
thread, adds the requester, and passes that thread as channelId. The requester
can then talk to Pup there without another mention. A newly created thread inherits
the channel’s visibility; it does not make a public request private. Tool audiences
and Discord permissions still apply. Commands and buttons keep their usual behavior.
Events and settings
events handles member.joined and member.left; return a pup presentation to
post in the channel the owner assigned, or nothing.
settings declares fields the server owner fills in on Pup’s dashboard: boolean,
text, long_text, number, select, channel, channels, role or roles.
Tools receive them as settings. There are no secret fields: keep secrets in your
own .env.
Approvals
Pup decides when a person must approve a write; manifests can’t change it.- A write needs approval when it is marked
alwaysAsk, or when outside content could have steered it: a web page, tool output, saved reply, server guidance, or a message from someone outside the tool’s audience. Incomplete source records also need review. Tools available to everyone never escalate. Reads don’t need approval. - The card shows the action and its inputs, with Do it, Auto-review, Don’t and Details. Only the asker can answer, within 15 minutes. Details is private and shows the complete inputs and excerpts of the outside content.
- Auto-review enables automatic review for that exact tool definition: next time, two checks run at once. One confirms the action matches what the asker asked for; a separate scan looks for injected instructions in the outside content. If both pass, the write runs and the reply notes it was auto-reviewed. Anything else shows the card. Changing the tool’s definition needs new consent, and members can turn Auto review off at any time.
alwaysAsktools always show the card and never offer Auto-review. Writes after hosted web search always need the asker.- Don’t, or letting the card expire, stops all trick writes for that request.
Limits and data
Defaults are 60 requests per installation per minute, and 20 calls per tool per minute. Pup pauses an installation after five consecutive failures; the owner turns it back on. Replies may link to exact URLs supplied by the asker or returned in an authorized trick result. Pup checks known malicious hosts and rejects disguised or credentialed links. Returned links have no previews and are never automatically opened or probed; showing one does not verify its destination or grantfetch_url access. The assistant
cannot construct a new URL from result data. Asker-only links still pass the existing
destination checks. Pup strips
mass and role pings, and only the asker can be mentioned.
No setting, scope or self-host option lets a trick moderate, change roles, channels or
permissions, send DMs, read other channels in bulk or post anywhere it likes. Pup keeps
full call inputs and results with their source so they can be deleted with it; audit
rows hold identifiers only. Uninstalling removes Pup’s copy, not data already sent to
your service, so document your own retention and deletion.
Protocol reference
Use this if you aren’t using the SDK, which implements all of it.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.