@mastra/discord 1.0.0 → 1.1.0-alpha.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 +39 -0
- package/LICENSE.md +32 -0
- package/README.md +175 -0
- package/dist/index.cjs +116012 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +554 -0
- package/dist/index.d.ts +554 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1110 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -50
- package/src/Discord.test.ts +0 -55
- package/src/assets/discord.png +0 -0
- package/src/index.ts +0 -201
- package/src/openapi-components.ts +0 -31769
- package/src/openapi-paths.ts +0 -15861
- package/src/openapi.ts +0 -23784
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# @mastra/discord
|
|
2
|
+
|
|
3
|
+
## 1.1.0-alpha.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)]:
|
|
39
|
+
- @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.
|