> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pup.run/llms.txt
> Use this file to discover all available pages before exploring further.

# Extension protocol

> Implement the signed manifest, MCP, task and event contracts without the SDK.

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.

```json theme={null}
{
  "manifestVersion": 1,
  "protocolVersion": "2026-07-28",
  "id": "snack_club",
  "name": "Snack Club",
  "author": "Example author",
  "description": "List available snacks.",
  "summary": "Read the available snack choices.",
  "instructions": "Use snack_menu when asked for the available snacks.",
  "endpoints": {
    "challenge": "https://snacks.example.com/challenge",
    "mcp": "https://snacks.example.com/mcp"
  },
  "tools": [{ "name": "snack_menu", "readOnly": true }],
  "commands": [{ "name": "snacks", "description": "See available snacks", "tool": "snack_menu" }],
  "settings": [],
  "events": []
}
```

| Field | Contract |
| - | - |
| `id`, tool `name`, setting `key` | Lowercase letter followed by lowercase letters, digits or underscores; at most 48 characters. Double underscores are reserved. The combined `id__tool` name must fit in 64 characters. |
| `summary` | One line, at most 150 characters; always available to the assistant. |
| `instructions` | At most 2,000 characters; loaded with tool details when needed. Instructions cannot grant access. |
| `tools` | 1–100 unique names matching the MCP tool list. `readOnly` defaults to false; `alwaysAsk` marks an irreversible write; `thread` requests a Discord thread; `userRequest` opts into the original triggering message. MCP annotations don't decide write policy. |
| `commands` | `name`, `description`, `tool`, optional `visibility` (`ephemeral` by default), `surfaces` (both by default) and `options`. Names can't reuse pup's own commands. |
| `settings` | Up to 25 fields, rendered by pup. Text defaults to a 500-character cap, long text to 4,000. |

### Signed requests

pup signs every MCP, challenge and event POST with
[Standard Webhooks](https://www.standardwebhooks.com/): `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](https://docs.pup.run/build-an-extension.md) when using the SDK; it handles this protocol for you.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.