@mastra/discord 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,77 @@
1
+ # @mastra/discord
2
+
3
+ ## 1.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Added `@mastra/discord` for connecting Mastra agents to Discord. One Discord app serves many servers, and agents respond to slash commands, DMs, and @mentions. Set `encryptionKey` or `MASTRA_ENCRYPTION_KEY` to encrypt the stored bot token at rest. ([#25002](https://github.com/mastra-ai/mastra/pull/25002))
8
+
9
+ ```ts
10
+ import { Mastra } from '@mastra/core';
11
+ import { DiscordProvider } from '@mastra/discord';
12
+
13
+ // App credentials from the Discord Developer Portal (or the DISCORD_BOT_TOKEN /
14
+ // DISCORD_PUBLIC_KEY / DISCORD_APPLICATION_ID env vars):
15
+ const discord = new DiscordProvider({
16
+ app: {
17
+ botToken: process.env.DISCORD_BOT_TOKEN!,
18
+ publicKey: process.env.DISCORD_PUBLIC_KEY!,
19
+ applicationId: process.env.DISCORD_APPLICATION_ID!,
20
+ },
21
+ });
22
+
23
+ export const mastra = new Mastra({
24
+ agents: { support },
25
+ channels: { discord },
26
+ });
27
+
28
+ // Bind an agent. If the bot is already in DISCORD_GUILD_ID, this binds the
29
+ // agent to that guild and registers its slash commands immediately. Otherwise
30
+ // it returns an OAuth2 bot-invite URL and the install stays pending until the
31
+ // bot joins a guild — the first interaction from that guild activates it.
32
+ const result = await discord.connect('support', { guildId: process.env.DISCORD_GUILD_ID });
33
+ // → { type: 'immediate' } OR { type: 'oauth', authorizationUrl, installationId }
34
+ ```
35
+
36
+ ### Patch Changes
37
+
38
+ - Updated dependencies [[`fc7d2c1`](https://github.com/mastra-ai/mastra/commit/fc7d2c102e911f43f70f425e67c970231ea19363), [`4607046`](https://github.com/mastra-ai/mastra/commit/460704663e2869183e7dfff7efec49a4f2f47503), [`1e435dc`](https://github.com/mastra-ai/mastra/commit/1e435dc84a9c1b35aa58d0ab9b14ff39fe13aab0), [`9ba23a2`](https://github.com/mastra-ai/mastra/commit/9ba23a23893622b72c76189199d02432590606c1), [`b757896`](https://github.com/mastra-ai/mastra/commit/b757896872edd74f71ec104be92273c5406265da), [`7f64865`](https://github.com/mastra-ai/mastra/commit/7f648656d2b24b214a899e8835b8286333c80a19), [`f751e65`](https://github.com/mastra-ai/mastra/commit/f751e659f496e5e53ed38632c59c296fec2ccbe5)]:
39
+ - @mastra/core@1.71.0
40
+
41
+ ## 1.1.0-alpha.0
42
+
43
+ ### Minor Changes
44
+
45
+ - Added `@mastra/discord` for connecting Mastra agents to Discord. One Discord app serves many servers, and agents respond to slash commands, DMs, and @mentions. Set `encryptionKey` or `MASTRA_ENCRYPTION_KEY` to encrypt the stored bot token at rest. ([#25002](https://github.com/mastra-ai/mastra/pull/25002))
46
+
47
+ ```ts
48
+ import { Mastra } from '@mastra/core';
49
+ import { DiscordProvider } from '@mastra/discord';
50
+
51
+ // App credentials from the Discord Developer Portal (or the DISCORD_BOT_TOKEN /
52
+ // DISCORD_PUBLIC_KEY / DISCORD_APPLICATION_ID env vars):
53
+ const discord = new DiscordProvider({
54
+ app: {
55
+ botToken: process.env.DISCORD_BOT_TOKEN!,
56
+ publicKey: process.env.DISCORD_PUBLIC_KEY!,
57
+ applicationId: process.env.DISCORD_APPLICATION_ID!,
58
+ },
59
+ });
60
+
61
+ export const mastra = new Mastra({
62
+ agents: { support },
63
+ channels: { discord },
64
+ });
65
+
66
+ // Bind an agent. If the bot is already in DISCORD_GUILD_ID, this binds the
67
+ // agent to that guild and registers its slash commands immediately. Otherwise
68
+ // it returns an OAuth2 bot-invite URL and the install stays pending until the
69
+ // bot joins a guild — the first interaction from that guild activates it.
70
+ const result = await discord.connect('support', { guildId: process.env.DISCORD_GUILD_ID });
71
+ // → { type: 'immediate' } OR { type: 'oauth', authorizationUrl, installationId }
72
+ ```
73
+
74
+ ### Patch Changes
75
+
76
+ - Updated dependencies [[`fc7d2c1`](https://github.com/mastra-ai/mastra/commit/fc7d2c102e911f43f70f425e67c970231ea19363), [`4607046`](https://github.com/mastra-ai/mastra/commit/460704663e2869183e7dfff7efec49a4f2f47503), [`1e435dc`](https://github.com/mastra-ai/mastra/commit/1e435dc84a9c1b35aa58d0ab9b14ff39fe13aab0), [`9ba23a2`](https://github.com/mastra-ai/mastra/commit/9ba23a23893622b72c76189199d02432590606c1)]:
77
+ - @mastra/core@1.71.0-alpha.1
package/LICENSE.md ADDED
@@ -0,0 +1,32 @@
1
+ Portions of this software are licensed as follows:
2
+
3
+ - All content that resides under any directory named `ee/` within this
4
+ repository, including but not limited to:
5
+ - `@mastra/core/auth/ee`
6
+ - `@mastra/core/agent-builder/ee`
7
+ - `@mastra/editor/ee`
8
+
9
+ is licensed under the license defined in [`ee/LICENSE`](https://github.com/mastra-ai/mastra/blob/main/ee/LICENSE).
10
+
11
+ - All third-party components incorporated into the Mastra Software are
12
+ licensed under the original license provided by the owner of the
13
+ applicable component.
14
+
15
+ - Content outside of the above-mentioned directories or restrictions is
16
+ available under the "Apache License 2.0" as defined below.
17
+
18
+ # Apache License 2.0
19
+
20
+ Copyright (c) 2025 Kepler Software, Inc.
21
+
22
+ Licensed under the Apache License, Version 2.0 (the "License");
23
+ you may not use this file except in compliance with the License.
24
+ You may obtain a copy of the License at
25
+
26
+ http://www.apache.org/licenses/LICENSE-2.0
27
+
28
+ Unless required by applicable law or agreed to in writing, software
29
+ distributed under the License is distributed on an "AS IS" BASIS,
30
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
31
+ See the License for the specific language governing permissions and
32
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,175 @@
1
+ # @mastra/discord
2
+
3
+ Discord channel wrapper for Mastra — a `ChannelProvider` (`@mastra/core/channels`) over **[`@chat-adapter/discord`](https://www.npmjs.com/package/@chat-adapter/discord)** (pinned to `4.40.0`), to parity with `@mastra/slack`.
4
+
5
+ The adapter already handles the Discord protocol (Ed25519 request verification, PING/PONG, deferrals, embeds + action-row buttons, the Gateway bridge, post-and-edit streaming). This package adds the install/lifecycle layer: the app-config + guild-keyed install store, an OAuth2 bot-invite `connect()`, guild/global slash-command registration, and Mastra route/stream wiring.
6
+
7
+ **Shape (how Discord differs from Slack/Telegram):** Discord has **no programmatic app creation** — apps are made in the Developer Portal. So there is no per-agent app factory: it's **one app, many guilds**. "Installing" is a **bot invite** (an OAuth2 authorize URL), not a token exchange — there is no per-install token to store. One bot token per application is reused across every guild, and tenancy is keyed by `guildId` per interaction.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install @mastra/discord
13
+ ```
14
+
15
+ Peer: `@mastra/core >= 1.22`.
16
+
17
+ ## Usage
18
+
19
+ ```ts
20
+ import { Mastra } from '@mastra/core';
21
+ import { DiscordProvider } from '@mastra/discord';
22
+
23
+ const discord = new DiscordProvider({
24
+ // From the Developer Portal. Or set DISCORD_BOT_TOKEN / DISCORD_PUBLIC_KEY /
25
+ // DISCORD_APPLICATION_ID in the environment and omit this.
26
+ app: {
27
+ botToken: process.env.DISCORD_BOT_TOKEN!,
28
+ publicKey: process.env.DISCORD_PUBLIC_KEY!,
29
+ applicationId: process.env.DISCORD_APPLICATION_ID!,
30
+ },
31
+ baseUrl: 'https://your-app.example.com', // for the interactions endpoint; auto-detected from the Mastra server if omitted
32
+ });
33
+
34
+ export const mastra = new Mastra({
35
+ agents: { support },
36
+ channels: { discord },
37
+ });
38
+
39
+ // If the bot is already in DISCORD_GUILD_ID, this binds the agent to that
40
+ // guild and registers its commands immediately. Otherwise it returns an
41
+ // OAuth2 bot-invite URL and the install stays pending until the bot joins a
42
+ // guild — at which point the first interaction from that guild activates it.
43
+ const result = await discord.connect('support', {
44
+ ...(process.env.DISCORD_GUILD_ID ? { guildId: process.env.DISCORD_GUILD_ID } : {}),
45
+ });
46
+ // → { type: 'immediate' } OR { type: 'oauth', authorizationUrl, installationId }
47
+ ```
48
+
49
+ See [`example/discord-agent.ts`](./example/discord-agent.ts) for a full runnable sketch.
50
+
51
+ ### Developer Portal setup
52
+
53
+ Discord apps are created in the [Developer Portal](https://discord.com/developers/applications), not via API. Once:
54
+
55
+ 1. **Application** → copy the **Application ID** and **Public Key** (General Information).
56
+ 2. **Bot** → add a bot, copy its **token**. Enable the **Message Content** / **Server Members** intents if you use DMs/mentions over the Gateway.
57
+ 3. **Interactions Endpoint URL** → set it to `${baseUrl}/discord/events/<webhookId>`. After calling `connect(agentId)`, retrieve the `webhookId` by calling `getInstallation(agentId)` — it returns the full `DiscordInstallation` including `webhookId`. (`connect()` returns `installationId`, and `listInstallations()` returns public info without `webhookId`.) Discord sends a signed test PING on save; the adapter answers it — don't rewrite the response.
58
+
59
+ ## Documentation
60
+
61
+ ### The connect flow
62
+
63
+ `connect(agentId, options?)` returns a discriminated `ChannelConnectResult`:
64
+
65
+ | Call | Result | Meaning |
66
+ | -------------------------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------- |
67
+ | `connect(id)` | `{ type: 'oauth', authorizationUrl, installationId }` | Open the URL, pick a server, authorize. The guild activates on the first interaction. |
68
+ | `connect(id, { guildId })` where the bot is **already** in `guildId` | `{ type: 'immediate' }` | Bound instantly; the guild's commands are registered. |
69
+ | `connect(id, { guildId })` where it is **not** | `{ type: 'oauth', authorizationUrl, installationId }` | Falls back to the invite URL. |
70
+
71
+ The bot-invite URL _is_ an OAuth2 authorize URL (`scope=bot applications.commands` + a `permissions` bitfield), so completion is a browser redirect — hence the `oauth` variant. Re-connecting an already-active agent throws (disconnect first). To add another server, invite the bot with the existing URL — **new guilds activate on their first interaction** off the interaction's authoritative `guild_id`.
72
+
73
+ Credentials are validated (`GET /applications/@me`) **before** anything is persisted, so a bad token leaves no config or install behind. The bot token is the only secret; with an `encryptionKey` set it is AES-256-GCM encrypted at rest.
74
+
75
+ ### Routing & the raw-body contract
76
+
77
+ The provider mounts **one** route, `POST /discord/events/:webhookId` (`requiresAuth: false` — Discord authenticates with Ed25519, not a bearer token). The handler passes the **raw request bytes** straight to `adapter.handleWebhook`; Ed25519 verification and PING/PONG live in the adapter.
78
+
79
+ > ⚠️ The Ed25519 signature is over `timestamp + raw body bytes`. Any middleware that parses and re-serializes the body (a JSON body-parser, `c.req.json()` then re-stringify) mutates the bytes and **every** signature fails. Don't put one in front of this route.
80
+
81
+ The mounted route only ever receives HTTP **Interactions** (PING, slash commands, buttons). DMs, @mentions and reactions arrive over the **Gateway**.
82
+
83
+ ### The Gateway is core's job
84
+
85
+ For each adapter with `gateway !== false` (default `true`), `@mastra/core` owns the persistent Gateway WebSocket + its reconnection loop (`AgentChannels` → `startGatewayLoop`). This wrapper just sets `gateway: true` on the adapter entry — it never calls `startGatewayListener` and never runs a reconnect loop. Set `gateway: false` for serverless deployments that only need slash commands over HTTP.
86
+
87
+ Because core owns the loop (there is no `stopGatewayListener`), `disconnect()` removes the install row and drops the adapter entry but **cannot** kill an in-flight Gateway window — it lapses at the next duration boundary.
88
+
89
+ ### Known caveat: reactions on a thread's starter message
90
+
91
+ An @mention in a channel opens a **new thread**, and Discord gives that thread an id equal to the starter message's id. `@chat-adapter/discord@4.40.0` handles this: when a reaction `PUT` against `/channels/{threadId}/messages/{messageId}/reactions/…` answers **404 `Unknown Message` (code 10008)** — the starter message is not a message _inside_ the thread, it is the message the thread hangs off — the adapter retries against the parent channel automatically. Replies were never affected: `AgentChannels` suppresses `tool-error` chunks for its own channel tools, so the agent's response still posts.
92
+
93
+ ### Commands
94
+
95
+ Slash commands default to a single `/help` seed. Override per agent or provider-wide:
96
+
97
+ ```ts
98
+ await discord.connect('support', {
99
+ commands: ['/help', { name: 'ask', description: 'Ask a question' }],
100
+ });
101
+ ```
102
+
103
+ Names are normalized to Discord's constraints (lowercase `[a-z0-9_-]`, 1-32 chars; description 1-100). Registration is scoped by `commandScope`:
104
+
105
+ - `'guild'` (default) — `PUT …/guilds/{id}/commands`, updates **instantly**, registered on each first-seen guild.
106
+ - `'global'` — `PUT …/commands`, eventually consistent, registered **once**.
107
+
108
+ Discord allows only **200 command creates per day, per guild**, so a per-scope content hash is tracked on the install and an unchanged command set is **not** re-registered.
109
+
110
+ ### Streaming & ephemeral
111
+
112
+ Discord has no native token streaming. With `streaming: true` (default) the reply chunk-edits the interaction followup via the adapter's `editMessage` loop — throttle it with `streaming: { updateIntervalMs }` (Discord rate-limits edits). `typingStatus: true` (default) keeps a typing indicator alive. `toolDisplay` defaults to `'cards'` (Discord has native embeds + action-row buttons).
113
+
114
+ The deferred type-5 ACK is sent synchronously inside the 3-second window; the agent then runs in the background (under `waitUntil`) and posts into the 15-minute followup window. Ephemeral replies (flag 64) are locked at defer time — decide them via the `interactionFlags` callback (fired on the initial deferred response).
115
+
116
+ ### Configuration
117
+
118
+ `new DiscordProvider(config)`:
119
+
120
+ | Option | Default | Notes |
121
+ | ------------------ | --------------------------------------- | ------------------------------------------------------------------------------------------------------ |
122
+ | `app` | `DISCORD_*` env | `{ botToken, publicKey, applicationId }`. Persisted once to channels storage. |
123
+ | `baseUrl` | Mastra server config | Public HTTPS base for the interactions endpoint. |
124
+ | `storage` | Mastra channels storage, else in-memory | Install persistence (`ChannelsStorage`). |
125
+ | `encryptionKey` | `MASTRA_ENCRYPTION_KEY` env | Encrypts the stored `botToken` at rest with AES-256-GCM when set; otherwise it is stored in plaintext. |
126
+ | `apiBaseUrl` | `https://discord.com/api/v10` | Override the REST origin (e.g. a test mock). |
127
+ | `permissions` | read/reply/embed/react bitfield | Invite-URL permissions (`bigint` or decimal string). |
128
+ | `commands` | `/help` | Default command seed. |
129
+ | `commandScope` | `'guild'` | `'guild'` \| `'global'`. |
130
+ | `gateway` | `true` | Start the core-owned Gateway loop (DMs/@mentions/reactions). |
131
+ | `streaming` | `true` | Post-and-edit reply streaming (`{ updateIntervalMs }` to tune). |
132
+ | `typingStatus` | `true` | Typing keepalive. |
133
+ | `toolDisplay` | `'cards'` | How tool calls render (Discord has native embeds/buttons). |
134
+ | `mentionRoleIds` | `DISCORD_MENTION_ROLE_IDS` env | Roles that trigger mention handlers. |
135
+ | `interactionFlags` | — | Return flags (e.g. ephemeral) for the initial deferred response. |
136
+ | `waitUntil` | — | Keep serverless invocations alive (Vercel/Lambda). |
137
+
138
+ ### AgentChannels passthrough
139
+
140
+ These forward to the agent's `AgentChannels` (the same curated subset `@mastra/slack` exposes); each falls back to anything the agent author already configured:
141
+
142
+ | Option | Notes |
143
+ | --------------------------------- | --------------------------------------------------------------------- |
144
+ | `handlers` | Override `onDirectMessage` / `onMention` / `onSubscribedMessage`. |
145
+ | `inlineMedia` | Which media types are sent inline to the model. |
146
+ | `inlineLinks` | Promote URLs in messages to file parts. |
147
+ | `tools` | Expose reaction tools (`add_reaction`/`remove_reaction`). Default on. |
148
+ | `state` | State adapter for dedup, locking, subscriptions. |
149
+ | `threadContext` | Fetch recent messages when joining a thread mid-conversation. |
150
+ | `chatOptions` | Passthrough to the underlying Chat SDK. |
151
+ | `resolveResourceId` | Choose memory ownership for a thread. |
152
+ | `cors` / `formatError` / `logger` | Interactions-route CORS, error rendering, adapter logger. |
153
+ | `resolveWaitUntil` | Resolve `waitUntil` from the request context. |
154
+ | `onInstall` | Called after an agent connects and the install is persisted. |
155
+
156
+ ### Module format
157
+
158
+ Dual **ESM + CJS**, mirroring `@mastra/slack`. The ESM-only `@chat-adapter/discord` is kept external in the ESM build but bundled into the CJS output, so both `import` and `require('@mastra/discord')` work.
159
+
160
+ ### Development
161
+
162
+ ```bash
163
+ npm install
164
+ npm run typecheck
165
+ npm test # vitest, undici-mocked Discord REST + a signed Ed25519 PING→PONG
166
+ npm run build # tsdown → dist (ESM + CJS + d.ts + d.cts)
167
+ ```
168
+
169
+ ## Changelog
170
+
171
+ See the [package changelog](https://github.com/mastra-ai/mastra/blob/main/channels/discord/CHANGELOG.md) for version history and release notes.
172
+
173
+ ## Support
174
+
175
+ We have an [open community Discord](https://discord.gg/mastra-ai). Come and say hello and let us know if you have any questions or need any help getting things running.