From a82c5c43b264d9ed68fd449aa49697f4282251f0 Mon Sep 17 00:00:00 2001 From: viniciusventura29 Date: Mon, 17 Nov 2025 20:18:39 -0300 Subject: [PATCH 1/3] feat: add Discord Bot MCP server and tools - Introduced a new workspace for the Discord Bot, including essential configurations in package.json and bun.lock. - Created the Discord Bot server with tools for managing messages, channels, roles, threads, and webhooks. - Added TypeScript configuration, Vite setup, and comprehensive documentation for the Discord Bot features and tools. - Implemented a client for interacting with the Discord API, ensuring robust data handling and validation. - Included a README.md detailing installation, configuration, and usage instructions for the Discord Bot MCP server. --- bun.lock | 22 + discord-bot/.gitignore | 1 + discord-bot/README.md | 151 +++ discord-bot/package.json | 34 + discord-bot/server/lib/constants.ts | 24 + discord-bot/server/lib/types.ts | 1159 +++++++++++++++++ discord-bot/server/main.ts | 62 + discord-bot/server/tools/channels.ts | 124 ++ discord-bot/server/tools/guilds.ts | 264 ++++ discord-bot/server/tools/index.ts | 46 + discord-bot/server/tools/messages.ts | 496 +++++++ discord-bot/server/tools/roles.ts | 201 +++ discord-bot/server/tools/threads.ts | 224 ++++ .../server/tools/utils/discord-client.ts | 257 ++++ discord-bot/server/tools/webhooks.ts | 240 ++++ discord-bot/shared/deco.gen.ts | 28 + discord-bot/tsconfig.json | 43 + discord-bot/vite.config.ts | 29 + discord-bot/wrangler.toml | 17 + package.json | 1 + 20 files changed, 3423 insertions(+) create mode 100644 discord-bot/.gitignore create mode 100644 discord-bot/README.md create mode 100644 discord-bot/package.json create mode 100644 discord-bot/server/lib/constants.ts create mode 100644 discord-bot/server/lib/types.ts create mode 100644 discord-bot/server/main.ts create mode 100644 discord-bot/server/tools/channels.ts create mode 100644 discord-bot/server/tools/guilds.ts create mode 100644 discord-bot/server/tools/index.ts create mode 100644 discord-bot/server/tools/messages.ts create mode 100644 discord-bot/server/tools/roles.ts create mode 100644 discord-bot/server/tools/threads.ts create mode 100644 discord-bot/server/tools/utils/discord-client.ts create mode 100644 discord-bot/server/tools/webhooks.ts create mode 100644 discord-bot/shared/deco.gen.ts create mode 100644 discord-bot/tsconfig.json create mode 100644 discord-bot/vite.config.ts create mode 100644 discord-bot/wrangler.toml diff --git a/bun.lock b/bun.lock index 331363fc..6434a428 100644 --- a/bun.lock +++ b/bun.lock @@ -94,6 +94,26 @@ "wrangler": "^4.28.0", }, }, + "discord-bot": { + "name": "discord-bot", + "version": "1.0.0", + "dependencies": { + "@decocms/runtime": "0.24.0", + "zod": "^3.24.3", + }, + "devDependencies": { + "@cloudflare/vite-plugin": "^1.13.4", + "@cloudflare/workers-types": "^4.20251014.0", + "@decocms/mcps-shared": "1.0.0", + "@mastra/core": "^0.24.0", + "@modelcontextprotocol/sdk": "^1.21.0", + "@types/mime-db": "^1.43.6", + "deco-cli": "^0.26.0", + "typescript": "^5.7.2", + "vite": "7.2.0", + "wrangler": "^4.28.0", + }, + }, "mcp-studio": { "name": "mcp-studio", "version": "1.0.0", @@ -1604,6 +1624,8 @@ "diff-match-patch": ["diff-match-patch@1.0.5", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="], + "discord-bot": ["discord-bot@workspace:discord-bot"], + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], "drizzle-orm": ["drizzle-orm@0.44.7", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ=="], diff --git a/discord-bot/.gitignore b/discord-bot/.gitignore new file mode 100644 index 00000000..babca1bb --- /dev/null +++ b/discord-bot/.gitignore @@ -0,0 +1 @@ +.dev.vars diff --git a/discord-bot/README.md b/discord-bot/README.md new file mode 100644 index 00000000..5405c119 --- /dev/null +++ b/discord-bot/README.md @@ -0,0 +1,151 @@ +# Discord Bot MCP + +MCP (Model Context Protocol) server para integração com Discord Bot API. + +## Funcionalidades + +### Mensagens +- Enviar mensagens em canais +- Editar mensagens existentes +- Deletar mensagens +- Fixar/desafixar mensagens +- Adicionar/remover reações +- Buscar mensagens de canais +- Buscar mensagens fixadas + +### Canais +- Criar canais (texto, voz, categorias) +- Listar canais do servidor +- Buscar informações de canais específicos + +### Servidores (Guilds) +- Listar servidores onde o bot está presente +- Buscar informações de servidores +- Listar membros do servidor +- Banir membros + +### Roles +- Criar roles +- Editar roles +- Deletar roles +- Listar roles do servidor + +### Threads +- Criar threads +- Entrar/sair de threads +- Listar threads ativas +- Listar threads arquivadas + +### Webhooks +- Criar webhooks +- Executar webhooks +- Deletar webhooks +- Listar webhooks + +### Usuários +- Buscar informações do usuário atual (bot) +- Buscar informações de usuários específicos + +## Configuração + +### Pré-requisitos + +1. Criar um bot no [Discord Developer Portal](https://discord.com/developers/applications) +2. Obter o Bot Token +3. Adicionar o bot ao seu servidor com as permissões necessárias + +### Instalação + +1. Instale o MCP no seu workspace Deco +2. Configure o Bot Token quando solicitado + +### Permissões Necessárias + +O bot precisa das seguintes permissões no Discord: +- Read Messages/View Channels +- Send Messages +- Manage Messages +- Manage Channels +- Manage Roles +- Manage Webhooks +- Ban Members +- Read Message History +- Add Reactions + +## Desenvolvimento + +```bash +# Instalar dependências +bun install + +# Desenvolvimento local +bun run dev + +# Build +bun run build + +# Deploy +bun run deploy +``` + +## Estrutura + +``` +discord-bot/ +├── server/ +│ ├── main.ts # Entry point do MCP +│ ├── lib/ +│ │ └── types.ts # Schemas Zod e tipos +│ └── tools/ +│ ├── index.ts # Exporta todas as tools +│ ├── messages.ts # Tools de mensagens +│ ├── channels.ts # Tools de canais +│ ├── guilds.ts # Tools de servidores +│ ├── roles.ts # Tools de roles +│ ├── threads.ts # Tools de threads +│ ├── webhooks.ts # Tools de webhooks +│ └── utils/ +│ └── discord-client.ts # Cliente HTTP Discord +└── shared/ + └── deco.gen.ts # Tipos gerados automaticamente +``` + +## Exemplos de Uso + +### Enviar uma mensagem + +```typescript +{ + "channelId": "123456789", + "content": "Hello, Discord!" +} +``` + +### Criar um canal + +```typescript +{ + "guildId": "123456789", + "name": "novo-canal", + "type": 0 +} +``` + +### Listar servidores + +```typescript +{ + "limit": 100 +} +``` + +## API do Discord + +Este MCP usa a [Discord API v10](https://discord.com/developers/docs/intro). + +## Suporte + +Para mais informações sobre a API do Discord, consulte: +- [Discord Developer Portal](https://discord.com/developers/docs) +- [Bot Permissions Calculator](https://discordapi.com/permissions.html) + diff --git a/discord-bot/package.json b/discord-bot/package.json new file mode 100644 index 00000000..158bae15 --- /dev/null +++ b/discord-bot/package.json @@ -0,0 +1,34 @@ +{ + "name": "discord-bot", + "version": "1.0.0", + "description": "MCP server for Discord Bot integration", + "private": true, + "type": "module", + "scripts": { + "dev": "deco dev --vite", + "configure": "deco configure", + "gen": "deco gen --output=shared/deco.gen.ts", + "deploy": "npm run build && deco deploy ./dist/server", + "check": "tsc --noEmit", + "build": "bun --bun vite build" + }, + "dependencies": { + "@decocms/runtime": "0.24.0", + "zod": "^3.24.3" + }, + "devDependencies": { + "@cloudflare/vite-plugin": "^1.13.4", + "@cloudflare/workers-types": "^4.20251014.0", + "@decocms/mcps-shared": "1.0.0", + "@mastra/core": "^0.24.0", + "@modelcontextprotocol/sdk": "^1.21.0", + "@types/mime-db": "^1.43.6", + "deco-cli": "^0.26.0", + "typescript": "^5.7.2", + "vite": "7.2.0", + "wrangler": "^4.28.0" + }, + "engines": { + "node": ">=22.0.0" + } +} diff --git a/discord-bot/server/lib/constants.ts b/discord-bot/server/lib/constants.ts new file mode 100644 index 00000000..0aea5935 --- /dev/null +++ b/discord-bot/server/lib/constants.ts @@ -0,0 +1,24 @@ +export const DISCORD_API_URL = "https://discord.com/api/v10"; + +export const DISCORD_ERROR_MESSAGES = { + UNAUTHORIZED: + "Bot token inválido ou expirado. Verifique suas credenciais do Discord.", + FORBIDDEN: + "Acesso negado. O bot não tem permissões necessárias para esta operação.", + NOT_FOUND: + "Recurso não encontrado. Verifique o ID do canal, servidor ou mensagem.", + BAD_REQUEST: "Requisição inválida. Verifique os parâmetros enviados.", + RATE_LIMITED: "Limite de requisições excedido. Tente novamente mais tarde.", + INTERNAL_SERVER_ERROR: "Erro interno do Discord. Tente novamente mais tarde.", + SERVICE_UNAVAILABLE: "Serviço do Discord temporariamente indisponível.", + GATEWAY_TIMEOUT: "Timeout na requisição. Tente novamente.", + INVALID_BOT_TOKEN: "Token do bot inválido ou malformado.", + MISSING_PERMISSIONS: + "O bot não possui as permissões necessárias no servidor.", + CHANNEL_NOT_FOUND: "Canal não encontrado ou não acessível.", + GUILD_NOT_FOUND: "Servidor não encontrado ou bot não está no servidor.", + MESSAGE_NOT_FOUND: "Mensagem não encontrada ou já foi deletada.", + USER_NOT_FOUND: "Usuário não encontrado ou não acessível.", + INVALID_MESSAGE_FORMAT: "Formato de mensagem inválido.", + MESSAGE_TOO_LONG: "Mensagem excede o limite de 2000 caracteres do Discord.", +} as const; diff --git a/discord-bot/server/lib/types.ts b/discord-bot/server/lib/types.ts new file mode 100644 index 00000000..aaded78a --- /dev/null +++ b/discord-bot/server/lib/types.ts @@ -0,0 +1,1159 @@ +export interface DiscordUser { + id: string; + username: string; + discriminator: string; + global_name?: string; + avatar?: string; + bot?: boolean; + system?: boolean; + mfa_enabled?: boolean; + banner?: string; + accent_color?: number; + locale?: string; + verified?: boolean; + email?: string; + flags?: number; + premium_type?: number; + public_flags?: number; +} + +export interface DiscordGuild { + id: string; + name: string; + icon?: string; + icon_hash?: string; + splash?: string; + discovery_splash?: string; + owner?: boolean; + owner_id: string; + permissions?: string; + region?: string; + afk_channel_id?: string; + afk_timeout: number; + widget_enabled?: boolean; + widget_channel_id?: string; + verification_level: number; + default_message_notifications: number; + explicit_content_filter: number; + roles: DiscordRole[]; + emojis: DiscordEmoji[]; + features: string[]; + mfa_level: number; + application_id?: string; + system_channel_id?: string; + system_channel_flags: number; + rules_channel_id?: string; + max_presences?: number; + max_members?: number; + vanity_url_code?: string; + description?: string; + banner?: string; + premium_tier: number; + premium_subscription_count?: number; + preferred_locale: string; + public_updates_channel_id?: string; + max_video_channel_users?: number; + approximate_member_count?: number; + approximate_presence_count?: number; + welcome_screen?: DiscordWelcomeScreen; + nsfw_level: number; + stickers?: DiscordSticker[]; + premium_progress_bar_enabled: boolean; +} + +export interface DiscordChannel { + id: string; + type: number; + guild_id?: string; + position?: number; + permission_overwrites?: DiscordPermissionOverwrite[]; + name?: string; + topic?: string; + nsfw?: boolean; + last_message_id?: string; + bitrate?: number; + user_limit?: number; + rate_limit_per_user?: number; + recipients?: DiscordUser[]; + icon?: string; + owner_id?: string; + application_id?: string; + parent_id?: string; + last_pin_timestamp?: string; + rtc_region?: string; + video_quality_mode?: number; + message_count?: number; + member_count?: number; + thread_metadata?: DiscordThreadMetadata; + member?: DiscordThreadMember; + default_auto_archive_duration?: number; + permissions?: string; +} + +export interface DiscordMessage { + id: string; + channel_id: string; + author: DiscordUser; + content: string; + timestamp: string; + edited_timestamp?: string; + tts: boolean; + mention_everyone: boolean; + mentions: DiscordUser[]; + mention_roles: string[]; + mention_channels?: DiscordChannelMention[]; + attachments: DiscordAttachment[]; + embeds: DiscordEmbed[]; + reactions?: DiscordReaction[]; + nonce?: string | number; + pinned: boolean; + webhook_id?: string; + type: number; + activity?: DiscordMessageActivity; + application?: DiscordApplication; + application_id?: string; + message_reference?: DiscordMessageReference; + flags?: number; + referenced_message?: DiscordMessage; + interaction?: DiscordMessageInteraction; + thread?: DiscordChannel; + components?: DiscordComponent[]; + sticker_items?: DiscordStickerItem[]; + position?: number; +} + +export interface DiscordEmbed { + title?: string; + type?: string; + description?: string; + url?: string; + timestamp?: string; + color?: number; + footer?: DiscordEmbedFooter; + image?: DiscordEmbedImage; + thumbnail?: DiscordEmbedThumbnail; + video?: DiscordEmbedVideo; + provider?: DiscordEmbedProvider; + author?: DiscordEmbedAuthor; + fields?: DiscordEmbedField[]; +} + +export interface DiscordEmbedFooter { + text: string; + icon_url?: string; + proxy_icon_url?: string; +} + +export interface DiscordEmbedImage { + url: string; + proxy_url?: string; + height?: number; + width?: number; +} + +export interface DiscordEmbedThumbnail { + url: string; + proxy_url?: string; + height?: number; + width?: number; +} + +export interface DiscordEmbedVideo { + url?: string; + proxy_url?: string; + height?: number; + width?: number; +} + +export interface DiscordEmbedProvider { + name?: string; + url?: string; +} + +export interface DiscordEmbedAuthor { + name: string; + url?: string; + icon_url?: string; + proxy_icon_url?: string; +} + +export interface DiscordEmbedField { + name: string; + value: string; + inline?: boolean; +} + +export interface DiscordAttachment { + id: string; + filename: string; + description?: string; + content_type?: string; + size: number; + url: string; + proxy_url: string; + height?: number; + width?: number; + ephemeral?: boolean; +} + +export interface DiscordReaction { + count: number; + me: boolean; + emoji: DiscordEmoji; +} + +export interface DiscordEmoji { + id?: string; + name?: string; + roles?: string[]; + user?: DiscordUser; + require_colons?: boolean; + managed?: boolean; + animated?: boolean; + available?: boolean; +} + +export interface DiscordRole { + id: string; + name: string; + color: number; + hoist: boolean; + icon?: string; + unicode_emoji?: string; + position: number; + permissions: string; + managed: boolean; + mentionable: boolean; + tags?: DiscordRoleTags; +} + +export interface DiscordRoleTags { + bot_id?: string; + integration_id?: string; + premium_subscriber?: null; +} + +export interface DiscordPermissionOverwrite { + id: string; + type: number; + allow: string; + deny: string; +} + +export interface DiscordThreadMetadata { + archived: boolean; + auto_archive_duration: number; + archive_timestamp: string; + locked: boolean; + invitable?: boolean; + create_timestamp?: string; +} + +export interface DiscordThreadMember { + id?: string; + user_id?: string; + join_timestamp: string; + flags: number; +} + +export interface DiscordChannelMention { + id: string; + guild_id: string; + type: number; + name: string; +} + +export interface DiscordMessageActivity { + type: number; + party_id?: string; +} + +export interface DiscordApplication { + id: string; + name: string; + icon?: string | null; + description: string; + rpc_origins?: string[]; + bot_public: boolean; + bot_require_code_grant: boolean; + terms_of_service_url?: string; + privacy_policy_url?: string; + owner?: DiscordUser; + verify_key: string; + team?: DiscordTeam | null; + guild_id?: string; + primary_sku_id?: string; + slug?: string; + cover_image?: string; + flags?: number; + tags?: string[]; + install_params?: DiscordInstallParams; + custom_install_url?: string; +} + +export interface DiscordTeam { + icon?: string | null; + id: string; + members: DiscordTeamMember[]; + name: string; + owner_user_id: string; +} + +export interface DiscordTeamMember { + membership_state: number; + permissions: string[]; + team_id: string; + user: DiscordUser; +} + +export interface DiscordInstallParams { + scopes: string[]; + permissions: string; +} + +export interface DiscordStageInstance { + id: string; + guild_id: string; + channel_id: string; + topic: string; + privacy_level: number; + discoverable_disabled: boolean; + guild_scheduled_event_id?: string | null; +} + +export type GuildScheduledEventStatus = 1 | 2 | 3 | 4; +export type GuildScheduledEventEntityType = 1 | 2 | 3; + +export interface DiscordGuildScheduledEvent { + id: string; + guild_id: string; + channel_id?: string | null; + creator_id?: string | null; + name: string; + description?: string | null; + scheduled_start_time: string; + scheduled_end_time?: string | null; + privacy_level: number; + status: GuildScheduledEventStatus; + entity_type: GuildScheduledEventEntityType; + entity_id?: string | null; + entity_metadata?: DiscordGuildScheduledEventEntityMetadata | null; + creator?: DiscordUser; + user_count?: number; + image?: string | null; +} + +export interface DiscordGuildScheduledEventEntityMetadata { + location?: string; +} + +export interface DiscordPartialEmoji { + id?: string | null; + name?: string | null; + animated?: boolean; +} + +export interface DiscordActionRow { + type: 1; + components: DiscordMessageComponent[]; +} + +export interface DiscordButton { + type: 2; + style: number; + label?: string; + emoji?: DiscordPartialEmoji; + custom_id?: string; + url?: string; + disabled?: boolean; +} + +export interface DiscordSelectMenu { + type: 3 | 5 | 6 | 7 | 8; + custom_id: string; + options?: DiscordSelectOption[]; + channel_types?: number[]; + placeholder?: string; + min_values?: number; + max_values?: number; + disabled?: boolean; +} + +export interface DiscordSelectOption { + label: string; + value: string; + description?: string; + emoji?: DiscordPartialEmoji; + default?: boolean; +} + +export interface DiscordTextInput { + type: 4; + custom_id: string; + style: number; + label: string; + min_length?: number; + max_length?: number; + required?: boolean; + value?: string; + placeholder?: string; +} + +export type DiscordMessageComponent = + | DiscordActionRow + | DiscordButton + | DiscordSelectMenu + | DiscordTextInput; + +export interface DiscordFile { + filename: string; + content_type?: string; + file: Blob | ArrayBuffer | Uint8Array; +} + +export interface DiscordMessageAttachment { + id?: string; + filename: string; + description?: string; + content_type?: string; + size?: number; + url?: string; + proxy_url?: string; + height?: number | null; + width?: number | null; + ephemeral?: boolean; +} + +export interface DiscordMessageReference { + message_id?: string; + channel_id?: string; + guild_id?: string; + fail_if_not_exists?: boolean; +} + +export interface DiscordMessageInteraction { + id: string; + type: number; + name: string; + user: DiscordUser; + member?: DiscordGuildMember; +} + +export interface DiscordGuildMember { + user?: DiscordUser; + nick?: string; + avatar?: string; + roles: string[]; + joined_at: string; + premium_since?: string; + deaf: boolean; + mute: boolean; + flags: number; + pending?: boolean; + permissions?: string; + communication_disabled_until?: string; +} + +export interface DiscordComponent { + type: number; + style?: number; + label?: string; + emoji?: DiscordEmoji; + custom_id?: string; + url?: string; + disabled?: boolean; + components?: DiscordComponent[]; +} + +export interface DiscordStickerItem { + id: string; + name: string; + format_type: number; +} + +export interface DiscordSticker { + id: string; + pack_id?: string; + name: string; + description?: string; + tags: string; + asset?: string; + type: number; + format_type: number; + available?: boolean; + guild_id?: string; + user?: DiscordUser; + sort_value?: number; +} + +export interface DiscordWelcomeScreen { + description?: string; + welcome_channels: DiscordWelcomeScreenChannel[]; +} + +export interface DiscordWelcomeScreenChannel { + channel_id: string; + description: string; + emoji_id?: string; + emoji_name?: string; +} + +export interface DiscordInvite { + type?: InviteType; + code: string; + guild?: DiscordGuild; + channel: DiscordChannel | null; + inviter?: DiscordUser; + target_type?: InviteTargetType; + target_user?: DiscordUser; + target_application?: DiscordApplication; + approximate_presence_count?: number; + approximate_member_count?: number; + expires_at?: string | null; + stage_instance?: DiscordStageInstance; + guild_scheduled_event?: DiscordGuildScheduledEvent; +} + +export interface DiscordGuildsResponse { + guilds: DiscordGuild[]; +} + +export interface EditRoleBody { + name?: string; + permissions?: string; + color?: number; + hoist?: boolean; + icon?: string; + unicode_emoji?: string; + mentionable?: boolean; + reason?: string; +} + +export type InviteType = 0 | 1 | 2; +export type InviteTargetType = 1 | 2; + +export interface AllowedMentions { + parse?: ("roles" | "users" | "everyone")[]; + roles?: string[]; + users?: string[]; + replied_user?: boolean; +} + +export interface MessageReference { + message_id?: string; + channel_id?: string; + guild_id?: string; + fail_if_not_exists?: boolean; +} + +export interface SendMessageBody { + content?: string; + nonce?: number | string; + tts?: boolean; + embeds?: DiscordEmbed[]; + allowed_mentions?: AllowedMentions; + message_reference?: MessageReference; + components?: DiscordMessageComponent[]; + files?: DiscordFile[]; + payload_json?: string; + attachments?: DiscordMessageAttachment[]; + flags?: number; + sticker_ids?: string[]; + thread_name?: string; +} + +export interface ExecuteWebhookBody { + content?: string; + username?: string; + avatar_url?: string; + tts?: boolean; + embeds?: DiscordEmbed[]; + thread_name?: string; + applied_tags?: string[]; +} + +export interface EditMessageBody { + content?: string; + embeds?: DiscordEmbed[]; +} + +export interface CreateWebhookBody { + name: string; + avatar?: string; +} + +export interface CreateThreadBody { + name: string; + auto_archive_duration?: number; + type?: number; + invitable?: boolean; + rate_limit_per_user?: number; + applied_tags?: string[]; + message?: { + content?: string; + embeds?: DiscordEmbed[]; + }; +} + +export interface CreateRoleBody { + name?: string; + permissions?: string; + color?: number; + hoist?: boolean; + icon?: string; + unicode_emoji?: string; + mentionable?: boolean; + reason?: string; +} + +// ======================================== +// Zod Schemas for MCP Tools +// ======================================== + +import { z } from "zod"; + +// ======================================== +// Message Schemas +// ======================================== + +export const discordEmbedSchema = z.object({ + title: z.string().optional(), + description: z.string().optional(), + url: z.string().optional(), + timestamp: z.string().optional(), + color: z.number().optional(), + footer: z + .object({ + text: z.string(), + icon_url: z.string().optional(), + }) + .optional(), + image: z + .object({ + url: z.string(), + }) + .optional(), + thumbnail: z + .object({ + url: z.string(), + }) + .optional(), + author: z + .object({ + name: z.string(), + url: z.string().optional(), + icon_url: z.string().optional(), + }) + .optional(), + fields: z + .array( + z.object({ + name: z.string(), + value: z.string(), + inline: z.boolean().optional(), + }), + ) + .optional(), +}); + +export const sendMessageInputSchema = z.object({ + channelId: z.string().describe("ID do canal Discord onde enviar a mensagem"), + content: z + .string() + .optional() + .describe("Conteúdo da mensagem (máximo 2000 caracteres)"), + tts: z.boolean().optional().describe("Enviar como texto para fala"), + embeds: z + .array(discordEmbedSchema) + .optional() + .describe("Lista de embeds para incluir na mensagem"), + replyToMessageId: z + .string() + .optional() + .describe("ID da mensagem para responder"), + replyMention: z + .boolean() + .optional() + .describe("Se deve mencionar o autor da mensagem original na resposta"), +}); + +export const sendMessageOutputSchema = z.object({ + id: z.string(), + channel_id: z.string(), + content: z.string(), + timestamp: z.string(), + author: z.object({ + id: z.string(), + username: z.string(), + discriminator: z.string(), + }), +}); + +export const editMessageInputSchema = z.object({ + channelId: z.string().describe("ID do canal Discord"), + messageId: z.string().describe("ID da mensagem a ser editada"), + content: z.string().optional().describe("Novo conteúdo da mensagem"), + embeds: z + .array(discordEmbedSchema) + .optional() + .describe("Novos embeds da mensagem"), +}); + +export const editMessageOutputSchema = sendMessageOutputSchema; + +export const deleteMessageInputSchema = z.object({ + channelId: z.string().describe("ID do canal Discord"), + messageId: z.string().describe("ID da mensagem a ser deletada"), +}); + +export const deleteMessageOutputSchema = z.object({ + success: z.boolean(), + message: z.string(), +}); + +export const getChannelMessagesInputSchema = z.object({ + channelId: z.string().describe("ID do canal Discord"), + limit: z + .number() + .min(1) + .max(100) + .optional() + .describe("Número máximo de mensagens a retornar (1-100)"), + before: z + .string() + .optional() + .describe("ID da mensagem - buscar mensagens antes desta"), + after: z + .string() + .optional() + .describe("ID da mensagem - buscar mensagens após esta"), + around: z + .string() + .optional() + .describe("ID da mensagem - buscar mensagens ao redor desta"), +}); + +export const getChannelMessagesOutputSchema = z.object({ + messages: z.array(sendMessageOutputSchema), +}); + +export const getMessageInputSchema = z.object({ + channelId: z.string().describe("ID do canal Discord"), + messageId: z.string().describe("ID da mensagem"), +}); + +export const getMessageOutputSchema = sendMessageOutputSchema; + +export const addReactionInputSchema = z.object({ + channelId: z.string().describe("ID do canal Discord"), + messageId: z.string().describe("ID da mensagem"), + emoji: z + .string() + .describe("Emoji para adicionar (Unicode ou custom emoji ID)"), +}); + +export const addReactionOutputSchema = z.object({ + success: z.boolean(), + message: z.string(), +}); + +export const removeReactionInputSchema = addReactionInputSchema; +export const removeReactionOutputSchema = addReactionOutputSchema; + +export const getMessageReactionsInputSchema = z.object({ + channelId: z.string().describe("ID do canal Discord"), + messageId: z.string().describe("ID da mensagem"), + emoji: z.string().describe("Emoji para buscar reações"), + limit: z + .number() + .min(1) + .max(100) + .optional() + .describe("Número máximo de usuários a retornar"), + after: z + .string() + .optional() + .describe("ID do usuário - buscar usuários após este"), +}); + +export const getMessageReactionsOutputSchema = z.object({ + users: z.array( + z.object({ + id: z.string(), + username: z.string(), + discriminator: z.string(), + }), + ), +}); + +export const pinMessageInputSchema = z.object({ + channelId: z.string().describe("ID do canal Discord"), + messageId: z.string().describe("ID da mensagem a ser fixada"), +}); + +export const pinMessageOutputSchema = z.object({ + success: z.boolean(), + message: z.string(), +}); + +export const unpinMessageInputSchema = pinMessageInputSchema; +export const unpinMessageOutputSchema = pinMessageOutputSchema; + +export const getPinnedMessagesInputSchema = z.object({ + channelId: z.string().describe("ID do canal Discord"), +}); + +export const getPinnedMessagesOutputSchema = z.object({ + messages: z.array(sendMessageOutputSchema), +}); + +// ======================================== +// Channel Schemas +// ======================================== + +export const createChannelInputSchema = z.object({ + guildId: z.string().describe("ID do servidor Discord"), + name: z.string().describe("Nome do canal (2-100 caracteres)"), + type: z + .number() + .optional() + .describe("Tipo do canal (0=Text, 2=Voice, 4=Category, 5=News)"), + topic: z.string().optional().describe("Tópico do canal"), + nsfw: z.boolean().optional().describe("Se o canal é NSFW"), + parentId: z.string().optional().describe("ID da categoria pai"), + position: z.number().optional().describe("Posição do canal na lista"), + bitrate: z.number().optional().describe("Taxa de bits do canal de voz"), + userLimit: z + .number() + .optional() + .describe("Limite de usuários do canal de voz"), + rateLimitPerUser: z + .number() + .optional() + .describe("Taxa de limite de mensagens por usuário"), +}); + +export const createChannelOutputSchema = z.object({ + id: z.string(), + type: z.number(), + name: z.string().optional(), + guild_id: z.string().optional(), +}); + +export const getGuildChannelsInputSchema = z.object({ + guildId: z.string().describe("ID do servidor Discord"), +}); + +export const getGuildChannelsOutputSchema = z.object({ + channels: z.array(createChannelOutputSchema), +}); + +// ======================================== +// Guild Schemas +// ======================================== + +export const listBotGuildsInputSchema = z.object({ + limit: z + .number() + .min(1) + .max(200) + .optional() + .describe("Número máximo de servidores a retornar"), + before: z + .string() + .optional() + .describe("ID do servidor - retornar servidores antes deste"), + after: z + .string() + .optional() + .describe("ID do servidor - retornar servidores após este"), + withCounts: z + .boolean() + .optional() + .describe("Se deve incluir contagem aproximada de membros"), +}); + +export const listBotGuildsOutputSchema = z.object({ + guilds: z.array( + z.object({ + id: z.string(), + name: z.string(), + icon: z.string().optional().nullable(), + owner: z.boolean().optional(), + permissions: z.string().optional(), + }), + ), +}); + +export const getGuildInputSchema = z.object({ + guildId: z.string().describe("ID do servidor Discord"), + withCounts: z + .boolean() + .optional() + .describe("Se deve incluir contagem de membros"), +}); + +export const getGuildOutputSchema = z.object({ + id: z.string(), + name: z.string(), + icon: z.string().optional().nullable(), + owner_id: z.string(), + permissions: z.string().optional(), + member_count: z.number().optional(), +}); + +export const getGuildMembersInputSchema = z.object({ + guildId: z.string().describe("ID do servidor Discord"), + limit: z + .number() + .min(1) + .max(1000) + .optional() + .describe("Número máximo de membros a retornar"), + after: z + .string() + .optional() + .describe("ID do usuário - buscar membros após este"), +}); + +export const getGuildMembersOutputSchema = z.object({ + members: z.array( + z.object({ + user: z + .object({ + id: z.string(), + username: z.string(), + discriminator: z.string(), + }) + .optional(), + nick: z.string().optional().nullable(), + roles: z.array(z.string()), + joined_at: z.string(), + }), + ), +}); + +export const banMemberInputSchema = z.object({ + guildId: z.string().describe("ID do servidor Discord"), + userId: z.string().describe("ID do usuário a ser banido"), + deleteMessageDays: z + .number() + .min(0) + .max(7) + .optional() + .describe("Número de dias de mensagens a deletar (0-7)"), + reason: z.string().optional().describe("Razão do banimento"), +}); + +export const banMemberOutputSchema = z.object({ + success: z.boolean(), + message: z.string(), +}); + +export const getCurrentUserInputSchema = z.object({}); + +export const getCurrentUserOutputSchema = z.object({ + id: z.string(), + username: z.string(), + discriminator: z.string(), + bot: z.boolean().optional(), +}); + +export const getUserInputSchema = z.object({ + userId: z.string().describe("ID do usuário"), +}); + +export const getUserOutputSchema = getCurrentUserOutputSchema; + +// ======================================== +// Role Schemas +// ======================================== + +export const createRoleInputSchema = z.object({ + guildId: z.string().describe("ID do servidor Discord"), + name: z.string().optional().describe("Nome da role"), + permissions: z.string().optional().describe("Permissões da role (bitfield)"), + color: z.number().optional().describe("Cor da role (número RGB)"), + hoist: z + .boolean() + .optional() + .describe("Se a role aparece separadamente na lista de membros"), + mentionable: z.boolean().optional().describe("Se a role pode ser mencionada"), +}); + +export const createRoleOutputSchema = z.object({ + id: z.string(), + name: z.string(), + color: z.number(), + hoist: z.boolean(), + position: z.number(), + permissions: z.string(), + managed: z.boolean(), + mentionable: z.boolean(), +}); + +export const editRoleInputSchema = z.object({ + guildId: z.string().describe("ID do servidor Discord"), + roleId: z.string().describe("ID da role a ser editada"), + name: z.string().optional().describe("Novo nome da role"), + permissions: z.string().optional().describe("Novas permissões da role"), + color: z.number().optional().describe("Nova cor da role"), + hoist: z.boolean().optional().describe("Se a role aparece separadamente"), + mentionable: z.boolean().optional().describe("Se a role pode ser mencionada"), +}); + +export const editRoleOutputSchema = createRoleOutputSchema; + +export const deleteRoleInputSchema = z.object({ + guildId: z.string().describe("ID do servidor Discord"), + roleId: z.string().describe("ID da role a ser deletada"), + reason: z.string().optional().describe("Razão da exclusão"), +}); + +export const deleteRoleOutputSchema = z.object({ + success: z.boolean(), + message: z.string(), +}); + +export const getGuildRolesInputSchema = z.object({ + guildId: z.string().describe("ID do servidor Discord"), +}); + +export const getGuildRolesOutputSchema = z.object({ + roles: z.array(createRoleOutputSchema), +}); + +// ======================================== +// Thread Schemas +// ======================================== + +export const createThreadInputSchema = z.object({ + channelId: z.string().describe("ID do canal Discord"), + name: z.string().describe("Nome da thread"), + autoArchiveDuration: z + .number() + .optional() + .describe("Duração para arquivamento automático em minutos"), + type: z.number().optional().describe("Tipo da thread"), + invitable: z + .boolean() + .optional() + .describe("Se membros podem convidar outros"), + rateLimitPerUser: z + .number() + .optional() + .describe("Taxa de limite de mensagens por usuário"), +}); + +export const createThreadOutputSchema = z.object({ + id: z.string(), + type: z.number(), + name: z.string().optional(), + guild_id: z.string().optional(), +}); + +export const joinThreadInputSchema = z.object({ + channelId: z.string().describe("ID da thread"), +}); + +export const joinThreadOutputSchema = z.object({ + success: z.boolean(), + message: z.string(), +}); + +export const leaveThreadInputSchema = joinThreadInputSchema; +export const leaveThreadOutputSchema = joinThreadOutputSchema; + +export const getActiveThreadsInputSchema = z.object({ + guildId: z.string().describe("ID do servidor Discord"), +}); + +export const getActiveThreadsOutputSchema = z.object({ + threads: z.array(createThreadOutputSchema), +}); + +export const getArchivedThreadsInputSchema = z.object({ + channelId: z.string().describe("ID do canal Discord"), + type: z.enum(["public", "private"]).describe("Tipo de threads arquivadas"), + before: z + .string() + .optional() + .describe( + "Timestamp ISO8601 - retornar threads arquivadas antes desta data", + ), + limit: z + .number() + .min(1) + .max(100) + .optional() + .describe("Número máximo de threads a retornar"), +}); + +export const getArchivedThreadsOutputSchema = getActiveThreadsOutputSchema; + +// ======================================== +// Webhook Schemas +// ======================================== + +export const createWebhookInputSchema = z.object({ + channelId: z.string().describe("ID do canal Discord"), + name: z.string().describe("Nome do webhook"), + avatar: z.string().optional().describe("URL ou data URI da imagem do avatar"), +}); + +export const createWebhookOutputSchema = z.object({ + id: z.string(), + type: z.number(), + name: z.string().optional().nullable(), + token: z.string().optional(), + channel_id: z.string(), +}); + +export const executeWebhookInputSchema = z.object({ + webhookId: z.string().describe("ID do webhook"), + webhookToken: z.string().describe("Token do webhook"), + content: z.string().optional().describe("Conteúdo da mensagem"), + username: z.string().optional().describe("Nome de usuário customizado"), + avatarUrl: z.string().optional().describe("URL do avatar customizado"), + tts: z.boolean().optional().describe("Se é uma mensagem de texto para fala"), + embeds: z.array(discordEmbedSchema).optional().describe("Lista de embeds"), + threadName: z + .string() + .optional() + .describe("Nome da thread a criar (para webhooks de fórum)"), + wait: z + .boolean() + .optional() + .describe("Se deve aguardar e retornar a mensagem criada"), + threadId: z + .string() + .optional() + .describe("ID da thread onde enviar a mensagem"), +}); + +export const executeWebhookOutputSchema = sendMessageOutputSchema; + +export const deleteWebhookInputSchema = z.object({ + webhookId: z.string().describe("ID do webhook"), + webhookToken: z.string().describe("Token do webhook"), +}); + +export const deleteWebhookOutputSchema = z.object({ + success: z.boolean(), + message: z.string(), +}); + +export const listWebhooksInputSchema = z.object({ + channelId: z + .string() + .optional() + .describe("ID do canal Discord (se omitido, requer guildId)"), + guildId: z + .string() + .optional() + .describe("ID do servidor Discord (se omitido, requer channelId)"), +}); + +export const listWebhooksOutputSchema = z.object({ + webhooks: z.array(createWebhookOutputSchema), +}); diff --git a/discord-bot/server/main.ts b/discord-bot/server/main.ts new file mode 100644 index 00000000..f81f4007 --- /dev/null +++ b/discord-bot/server/main.ts @@ -0,0 +1,62 @@ +/** + * This is the main entry point for the Discord Bot MCP server. + * This is a Cloudflare workers app, and serves your MCP server at /mcp. + */ +import { z } from "zod"; +import { DefaultEnv, withRuntime } from "@decocms/runtime"; +import { + type Env as DecoEnv, + StateSchema as BaseStateSchema, +} from "../shared/deco.gen.ts"; + +import { tools } from "./tools/index.ts"; + +/** + * State schema for Discord Bot MCP configuration. + * Users fill these values when installing the MCP. + */ +export const StateSchema = BaseStateSchema.extend({ + botToken: z + .string() + .describe( + "Discord Bot Token from Developer Portal (https://discord.com/developers/applications)", + ), +}); + +/** + * This Env type is the main context object that is passed to + * all of your Application. + * + * It includes all of the generated types from your + * Deco bindings, along with the default ones. + */ +export type Env = DefaultEnv & + DecoEnv & { + ASSETS: { + fetch: (request: Request, init?: RequestInit) => Promise; + }; + }; + +const runtime = withRuntime({ + oauth: { + /** + * These scopes define the asking permissions of your + * app when a user is installing it. + */ + scopes: [], + /** + * The state schema of your Application defines what + * your installed App state will look like. When a user + * is installing your App, they will have to fill in + * a form with the fields defined in the state schema. + */ + state: StateSchema, + }, + tools, + /** + * Fallback directly to assets for all requests that do not match a tool or auth. + */ + fetch: (req: Request, env: Env) => env.ASSETS.fetch(req), +}); + +export default runtime; diff --git a/discord-bot/server/tools/channels.ts b/discord-bot/server/tools/channels.ts new file mode 100644 index 00000000..862374de --- /dev/null +++ b/discord-bot/server/tools/channels.ts @@ -0,0 +1,124 @@ +/** + * MCP tools for Discord channel operations + * + * This file implements tools for: + * - Creating channels + * - Listing guild channels + */ + +import type { Env } from "../main.ts"; +import { createDiscordClient } from "./utils/discord-client.ts"; +import { createPrivateTool } from "@decocms/runtime/mastra"; +import { + createChannelInputSchema, + createChannelOutputSchema, + getGuildChannelsInputSchema, + getGuildChannelsOutputSchema, +} from "../lib/types.ts"; + +/** + * CREATE_CHANNEL - Create a new channel in a Discord guild + */ +export const createCreateChannelTool = (env: Env) => + createPrivateTool({ + id: "CREATE_CHANNEL", + description: + "Create a new channel in a Discord server (guild). Supports text channels, voice channels, categories, and announcement channels. You can configure permissions, position, and various channel settings.", + inputSchema: createChannelInputSchema, + outputSchema: createChannelOutputSchema, + execute: async ({ context }) => { + const { + guildId, + name, + type = 0, + topic, + nsfw = false, + parentId, + position, + bitrate, + userLimit, + rateLimitPerUser, + } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + if (!name || name.length < 2 || name.length > 100) { + throw new Error("Channel name must be between 2 and 100 characters"); + } + + const client = createDiscordClient({ botToken: state.botToken }); + + const body: any = { + name, + type, + nsfw, + }; + + if (topic) { + if (topic.length > 1024) { + throw new Error("Channel topic cannot exceed 1024 characters"); + } + body.topic = topic; + } + + if (parentId) body.parent_id = parentId; + if (position !== undefined) body.position = position; + if (bitrate !== undefined) body.bitrate = bitrate; + if (userLimit !== undefined) body.user_limit = userLimit; + if (rateLimitPerUser !== undefined) + body.rate_limit_per_user = rateLimitPerUser; + + try { + const channel = await client.createChannel(guildId, body); + return { + id: channel.id, + type: channel.type, + name: channel.name, + guild_id: channel.guild_id, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to create channel: ${message}`); + } + }, + }); + +/** + * GET_GUILD_CHANNELS - Get all channels in a Discord guild + */ +export const createGetGuildChannelsTool = (env: Env) => + createPrivateTool({ + id: "GET_GUILD_CHANNELS", + description: + "Fetch all channels from a Discord server (guild). Returns a list of all channels including text, voice, categories, and announcement channels with their metadata.", + inputSchema: getGuildChannelsInputSchema, + outputSchema: getGuildChannelsOutputSchema, + execute: async ({ context }) => { + const { guildId } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + const client = createDiscordClient({ botToken: state.botToken }); + + try { + const channels = await client.getGuildChannels(guildId); + return { + channels: channels.map((channel: any) => ({ + id: channel.id, + type: channel.type, + name: channel.name, + guild_id: channel.guild_id, + })), + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to get guild channels: ${message}`); + } + }, + }); + +/** + * Array of all channel-related tools + */ +export const channelTools = [ + createCreateChannelTool, + createGetGuildChannelsTool, +]; diff --git a/discord-bot/server/tools/guilds.ts b/discord-bot/server/tools/guilds.ts new file mode 100644 index 00000000..36128194 --- /dev/null +++ b/discord-bot/server/tools/guilds.ts @@ -0,0 +1,264 @@ +/** + * MCP tools for Discord guild (server) operations + * + * This file implements tools for: + * - Listing bot guilds + * - Getting guild information + * - Managing guild members + * - User operations + */ + +import type { Env } from "../main.ts"; +import { createDiscordClient } from "./utils/discord-client.ts"; +import { createPrivateTool } from "@decocms/runtime/mastra"; +import { + listBotGuildsInputSchema, + listBotGuildsOutputSchema, + getGuildInputSchema, + getGuildOutputSchema, + getGuildMembersInputSchema, + getGuildMembersOutputSchema, + banMemberInputSchema, + banMemberOutputSchema, + getCurrentUserInputSchema, + getCurrentUserOutputSchema, + getUserInputSchema, + getUserOutputSchema, +} from "../lib/types.ts"; + +/** + * LIST_BOT_GUILDS - List all guilds where the bot is present + */ +export const createListBotGuildsTool = (env: Env) => + createPrivateTool({ + id: "LIST_BOT_GUILDS", + description: + "List all Discord servers (guilds) where the bot is currently a member. Returns guild names, IDs, icons, and permissions. Supports pagination.", + inputSchema: listBotGuildsInputSchema, + outputSchema: listBotGuildsOutputSchema, + execute: async ({ context }) => { + const { limit = 100, before, after, withCounts = false } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + const client = createDiscordClient({ botToken: state.botToken }); + + const searchParams: Record = { + limit: Math.min(Math.max(limit, 1), 200).toString(), + }; + + if (before) searchParams.before = before; + if (after) searchParams.after = after; + if (withCounts) searchParams.with_counts = "true"; + + try { + const guilds = await client.listBotGuilds(searchParams); + return { + guilds: guilds.map((guild: any) => ({ + id: guild.id, + name: guild.name, + icon: guild.icon, + owner: guild.owner, + permissions: guild.permissions, + })), + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to list bot guilds: ${message}`); + } + }, + }); + +/** + * GET_GUILD - Get information about a specific guild + */ +export const createGetGuildTool = (env: Env) => + createPrivateTool({ + id: "GET_GUILD", + description: + "Fetch detailed information about a specific Discord server (guild) including name, icon, owner, and member counts. Requires the bot to be a member of the guild.", + inputSchema: getGuildInputSchema, + outputSchema: getGuildOutputSchema, + execute: async ({ context }) => { + const { guildId, withCounts = false } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + const client = createDiscordClient({ botToken: state.botToken }); + + const searchParams: Record = {}; + if (withCounts) searchParams.with_counts = "true"; + + try { + const guild = await client.getGuild(guildId, searchParams); + return { + id: guild.id, + name: guild.name, + icon: guild.icon, + owner_id: guild.owner_id, + permissions: guild.permissions, + member_count: guild.approximate_member_count, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to get guild: ${message}`); + } + }, + }); + +/** + * GET_GUILD_MEMBERS - Get members from a guild + */ +export const createGetGuildMembersTool = (env: Env) => + createPrivateTool({ + id: "GET_GUILD_MEMBERS", + description: + "Fetch members from a Discord server (guild). Returns user information, nicknames, roles, and join dates. Supports pagination with up to 1000 members per request. Requires the GUILD_MEMBERS privileged intent.", + inputSchema: getGuildMembersInputSchema, + outputSchema: getGuildMembersOutputSchema, + execute: async ({ context }) => { + const { guildId, limit = 100, after } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + const client = createDiscordClient({ botToken: state.botToken }); + + const searchParams: Record = { + limit: Math.min(Math.max(limit, 1), 1000).toString(), + }; + + if (after) searchParams.after = after; + + try { + const members = await client.getGuildMembers(guildId, searchParams); + return { + members: members.map((member: any) => ({ + user: member.user + ? { + id: member.user.id, + username: member.user.username, + discriminator: member.user.discriminator, + } + : undefined, + nick: member.nick, + roles: member.roles, + joined_at: member.joined_at, + })), + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to get guild members: ${message}`); + } + }, + }); + +/** + * BAN_MEMBER - Ban a member from a guild + */ +export const createBanMemberTool = (env: Env) => + createPrivateTool({ + id: "BAN_MEMBER", + description: + "Ban a user from a Discord server (guild). Can optionally delete messages from the banned user from the last 0-7 days. Requires BAN_MEMBERS permission.", + inputSchema: banMemberInputSchema, + outputSchema: banMemberOutputSchema, + execute: async ({ context }) => { + const { guildId, userId, deleteMessageDays = 0, reason } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + if (deleteMessageDays < 0 || deleteMessageDays > 7) { + throw new Error("deleteMessageDays must be between 0 and 7"); + } + + const client = createDiscordClient({ botToken: state.botToken }); + + const body: any = { + delete_message_days: deleteMessageDays, + }; + + if (reason) { + body.reason = reason; + } + + try { + await client.banMember(guildId, userId, body); + return { + success: true, + message: "Member banned successfully", + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to ban member: ${message}`); + } + }, + }); + +/** + * GET_CURRENT_USER - Get information about the bot's user account + */ +export const createGetCurrentUserTool = (env: Env) => + createPrivateTool({ + id: "GET_CURRENT_USER", + description: + "Get information about the bot's own user account including username, discriminator, ID, and bot status.", + inputSchema: getCurrentUserInputSchema, + outputSchema: getCurrentUserOutputSchema, + execute: async () => { + const state = env.DECO_REQUEST_CONTEXT.state; + + const client = createDiscordClient({ botToken: state.botToken }); + + try { + const user = await client.getCurrentUser(); + return { + id: user.id, + username: user.username, + discriminator: user.discriminator, + bot: user.bot, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to get current user: ${message}`); + } + }, + }); + +/** + * GET_USER - Get information about a specific user + */ +export const createGetUserTool = (env: Env) => + createPrivateTool({ + id: "GET_USER", + description: + "Get information about a specific Discord user by their user ID. Returns username, discriminator, and public profile information.", + inputSchema: getUserInputSchema, + outputSchema: getUserOutputSchema, + execute: async ({ context }) => { + const { userId } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + const client = createDiscordClient({ botToken: state.botToken }); + + try { + const user = await client.getUser(userId); + return { + id: user.id, + username: user.username, + discriminator: user.discriminator, + bot: user.bot, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to get user: ${message}`); + } + }, + }); + +/** + * Array of all guild-related tools + */ +export const guildTools = [ + createListBotGuildsTool, + createGetGuildTool, + createGetGuildMembersTool, + createBanMemberTool, + createGetCurrentUserTool, + createGetUserTool, +]; diff --git a/discord-bot/server/tools/index.ts b/discord-bot/server/tools/index.ts new file mode 100644 index 00000000..63993acb --- /dev/null +++ b/discord-bot/server/tools/index.ts @@ -0,0 +1,46 @@ +/** + * Central export point for all Discord Bot MCP tools. + * + * This file aggregates all tools from different domains into a single + * export, making it easy to import all tools in main.ts while keeping + * the domain separation. + */ + +import { userTools } from "@decocms/mcps-shared/tools/user"; +import { messageTools } from "./messages.ts"; +import { channelTools } from "./channels.ts"; +import { guildTools } from "./guilds.ts"; +import { roleTools } from "./roles.ts"; +import { threadTools } from "./threads.ts"; +import { webhookTools } from "./webhooks.ts"; + +/** + * Export all tools from all domains + * + * Tools are organized by functionality: + * - userTools: User authentication and profile tools + * - messageTools: Message operations (send, edit, delete, reactions, pins) + * - channelTools: Channel management + * - guildTools: Server/guild and member management + * - roleTools: Role management + * - threadTools: Thread operations + * - webhookTools: Webhook management + */ +export const tools = [ + ...userTools, + ...messageTools, + ...channelTools, + ...guildTools, + ...roleTools, + ...threadTools, + ...webhookTools, +]; + +// Re-export domain-specific tools for direct access if needed +export { userTools } from "@decocms/mcps-shared/tools/user"; +export { messageTools } from "./messages.ts"; +export { channelTools } from "./channels.ts"; +export { guildTools } from "./guilds.ts"; +export { roleTools } from "./roles.ts"; +export { threadTools } from "./threads.ts"; +export { webhookTools } from "./webhooks.ts"; diff --git a/discord-bot/server/tools/messages.ts b/discord-bot/server/tools/messages.ts new file mode 100644 index 00000000..1b8ae008 --- /dev/null +++ b/discord-bot/server/tools/messages.ts @@ -0,0 +1,496 @@ +/** + * MCP tools for Discord message operations + * + * This file implements tools for: + * - Sending, editing, and deleting messages + * - Adding and removing reactions + * - Pinning and unpinning messages + * - Fetching messages and reactions + */ + +import type { Env } from "../main.ts"; +import { createDiscordClient } from "./utils/discord-client.ts"; +import { createPrivateTool } from "@decocms/runtime/mastra"; +import { + sendMessageInputSchema, + sendMessageOutputSchema, + editMessageInputSchema, + editMessageOutputSchema, + deleteMessageInputSchema, + deleteMessageOutputSchema, + getChannelMessagesInputSchema, + getChannelMessagesOutputSchema, + getMessageInputSchema, + getMessageOutputSchema, + addReactionInputSchema, + addReactionOutputSchema, + removeReactionInputSchema, + removeReactionOutputSchema, + getMessageReactionsInputSchema, + getMessageReactionsOutputSchema, + pinMessageInputSchema, + pinMessageOutputSchema, + unpinMessageInputSchema, + unpinMessageOutputSchema, + getPinnedMessagesInputSchema, + getPinnedMessagesOutputSchema, +} from "../lib/types.ts"; + +/** + * SEND_MESSAGE - Send a message to a Discord channel + */ +export const createSendMessageTool = (env: Env) => + createPrivateTool({ + id: "SEND_MESSAGE", + description: + "Send a message to a Discord channel. Supports text content, embeds, TTS, and replies. Messages can include rich formatting and mentions.", + inputSchema: sendMessageInputSchema, + outputSchema: sendMessageOutputSchema, + execute: async ({ context }) => { + const { + channelId, + content, + tts = false, + embeds, + replyToMessageId, + replyMention = false, + } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + if (!content && (!embeds || embeds.length === 0)) { + throw new Error("Message content or embeds are required"); + } + + if (content && content.length > 2000) { + throw new Error("Message content cannot exceed 2000 characters"); + } + + const client = createDiscordClient({ botToken: state.botToken }); + + const body: any = { + tts, + }; + + if (content) { + body.content = content; + } + + if (embeds && embeds.length > 0) { + body.embeds = embeds; + } + + if (replyToMessageId) { + body.message_reference = { + message_id: replyToMessageId, + }; + body.allowed_mentions = { + replied_user: replyMention, + }; + } + + try { + const message = await client.sendMessage(channelId, body); + return { + id: message.id, + channel_id: message.channel_id, + content: message.content, + timestamp: message.timestamp, + author: { + id: message.author.id, + username: message.author.username, + discriminator: message.author.discriminator, + }, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to send message: ${message}`); + } + }, + }); + +/** + * EDIT_MESSAGE - Edit an existing Discord message + */ +export const createEditMessageTool = (env: Env) => + createPrivateTool({ + id: "EDIT_MESSAGE", + description: + "Edit an existing message in a Discord channel. You can update the content and/or embeds. Only messages sent by the bot can be edited.", + inputSchema: editMessageInputSchema, + outputSchema: editMessageOutputSchema, + execute: async ({ context }) => { + const { channelId, messageId, content, embeds } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + if (!content && (!embeds || embeds.length === 0)) { + throw new Error("Content or embeds are required for editing"); + } + + const client = createDiscordClient({ botToken: state.botToken }); + + const body: any = {}; + + if (content) { + if (content.length > 2000) { + throw new Error("Message content cannot exceed 2000 characters"); + } + body.content = content; + } + + if (embeds && embeds.length > 0) { + body.embeds = embeds; + } + + try { + const message = await client.editMessage(channelId, messageId, body); + return { + id: message.id, + channel_id: message.channel_id, + content: message.content, + timestamp: message.timestamp, + author: { + id: message.author.id, + username: message.author.username, + discriminator: message.author.discriminator, + }, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to edit message: ${message}`); + } + }, + }); + +/** + * DELETE_MESSAGE - Delete a Discord message + */ +export const createDeleteMessageTool = (env: Env) => + createPrivateTool({ + id: "DELETE_MESSAGE", + description: + "Delete a message from a Discord channel. The bot needs Manage Messages permission to delete messages from other users, or can always delete its own messages.", + inputSchema: deleteMessageInputSchema, + outputSchema: deleteMessageOutputSchema, + execute: async ({ context }) => { + const { channelId, messageId } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + const client = createDiscordClient({ botToken: state.botToken }); + + try { + await client.deleteMessage(channelId, messageId); + return { + success: true, + message: "Message deleted successfully", + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to delete message: ${message}`); + } + }, + }); + +/** + * GET_CHANNEL_MESSAGES - Get messages from a Discord channel + */ +export const createGetChannelMessagesTool = (env: Env) => + createPrivateTool({ + id: "GET_CHANNEL_MESSAGES", + description: + "Fetch messages from a Discord channel. Supports pagination with before, after, and around parameters. Returns up to 100 messages per request.", + inputSchema: getChannelMessagesInputSchema, + outputSchema: getChannelMessagesOutputSchema, + execute: async ({ context }) => { + const { channelId, limit = 50, before, after, around } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + const client = createDiscordClient({ botToken: state.botToken }); + + const searchParams: Record = { + limit: Math.min(Math.max(limit, 1), 100).toString(), + }; + + if (before) searchParams.before = before; + if (after) searchParams.after = after; + if (around) searchParams.around = around; + + try { + const messages = await client.getChannelMessages( + channelId, + searchParams, + ); + return { + messages: messages.map((msg: any) => ({ + id: msg.id, + channel_id: msg.channel_id, + content: msg.content, + timestamp: msg.timestamp, + author: { + id: msg.author.id, + username: msg.author.username, + discriminator: msg.author.discriminator, + }, + })), + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to get channel messages: ${message}`); + } + }, + }); + +/** + * GET_MESSAGE - Get a specific Discord message + */ +export const createGetMessageTool = (env: Env) => + createPrivateTool({ + id: "GET_MESSAGE", + description: + "Fetch a specific message by ID from a Discord channel. Returns the full message object with all metadata.", + inputSchema: getMessageInputSchema, + outputSchema: getMessageOutputSchema, + execute: async ({ context }) => { + const { channelId, messageId } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + const client = createDiscordClient({ botToken: state.botToken }); + + try { + const message = await client.getMessage(channelId, messageId); + return { + id: message.id, + channel_id: message.channel_id, + content: message.content, + timestamp: message.timestamp, + author: { + id: message.author.id, + username: message.author.username, + discriminator: message.author.discriminator, + }, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to get message: ${message}`); + } + }, + }); + +/** + * ADD_REACTION - Add a reaction to a Discord message + */ +export const createAddReactionTool = (env: Env) => + createPrivateTool({ + id: "ADD_REACTION", + description: + "Add a reaction (emoji) to a Discord message. Supports both Unicode emojis and custom server emojis. For custom emojis, use the format 'name:id'.", + inputSchema: addReactionInputSchema, + outputSchema: addReactionOutputSchema, + execute: async ({ context }) => { + const { channelId, messageId, emoji } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + const client = createDiscordClient({ botToken: state.botToken }); + + try { + // Encode the emoji for URL + const encodedEmoji = encodeURIComponent(emoji); + await client.addReaction(channelId, messageId, encodedEmoji); + return { + success: true, + message: "Reaction added successfully", + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to add reaction: ${message}`); + } + }, + }); + +/** + * REMOVE_REACTION - Remove a reaction from a Discord message + */ +export const createRemoveReactionTool = (env: Env) => + createPrivateTool({ + id: "REMOVE_REACTION", + description: + "Remove the bot's reaction from a Discord message. Only removes reactions added by the bot itself.", + inputSchema: removeReactionInputSchema, + outputSchema: removeReactionOutputSchema, + execute: async ({ context }) => { + const { channelId, messageId, emoji } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + const client = createDiscordClient({ botToken: state.botToken }); + + try { + // Encode the emoji for URL + const encodedEmoji = encodeURIComponent(emoji); + await client.removeReaction(channelId, messageId, encodedEmoji); + return { + success: true, + message: "Reaction removed successfully", + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to remove reaction: ${message}`); + } + }, + }); + +/** + * GET_MESSAGE_REACTIONS - Get users who reacted with a specific emoji + */ +export const createGetMessageReactionsTool = (env: Env) => + createPrivateTool({ + id: "GET_MESSAGE_REACTIONS", + description: + "Get a list of users who reacted to a message with a specific emoji. Returns up to 100 users per request with pagination support.", + inputSchema: getMessageReactionsInputSchema, + outputSchema: getMessageReactionsOutputSchema, + execute: async ({ context }) => { + const { channelId, messageId, emoji, limit = 25, after } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + const client = createDiscordClient({ botToken: state.botToken }); + + const searchParams: Record = { + limit: Math.min(Math.max(limit, 1), 100).toString(), + }; + + if (after) searchParams.after = after; + + try { + // Encode the emoji for URL + const encodedEmoji = encodeURIComponent(emoji); + const users = await client.getMessageReactions( + channelId, + messageId, + encodedEmoji, + searchParams, + ); + return { + users: users.map((user: any) => ({ + id: user.id, + username: user.username, + discriminator: user.discriminator, + })), + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to get message reactions: ${message}`); + } + }, + }); + +/** + * PIN_MESSAGE - Pin a message in a Discord channel + */ +export const createPinMessageTool = (env: Env) => + createPrivateTool({ + id: "PIN_MESSAGE", + description: + "Pin a message in a Discord channel. Pinned messages appear at the top of the channel. Channels can have up to 50 pinned messages.", + inputSchema: pinMessageInputSchema, + outputSchema: pinMessageOutputSchema, + execute: async ({ context }) => { + const { channelId, messageId } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + const client = createDiscordClient({ botToken: state.botToken }); + + try { + await client.pinMessage(channelId, messageId); + return { + success: true, + message: "Message pinned successfully", + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to pin message: ${message}`); + } + }, + }); + +/** + * UNPIN_MESSAGE - Unpin a message from a Discord channel + */ +export const createUnpinMessageTool = (env: Env) => + createPrivateTool({ + id: "UNPIN_MESSAGE", + description: + "Unpin a previously pinned message from a Discord channel. Removes the message from the pinned messages list.", + inputSchema: unpinMessageInputSchema, + outputSchema: unpinMessageOutputSchema, + execute: async ({ context }) => { + const { channelId, messageId } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + const client = createDiscordClient({ botToken: state.botToken }); + + try { + await client.unpinMessage(channelId, messageId); + return { + success: true, + message: "Message unpinned successfully", + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to unpin message: ${message}`); + } + }, + }); + +/** + * GET_PINNED_MESSAGES - Get all pinned messages from a channel + */ +export const createGetPinnedMessagesTool = (env: Env) => + createPrivateTool({ + id: "GET_PINNED_MESSAGES", + description: + "Fetch all pinned messages from a Discord channel. Returns a list of up to 50 pinned messages.", + inputSchema: getPinnedMessagesInputSchema, + outputSchema: getPinnedMessagesOutputSchema, + execute: async ({ context }) => { + const { channelId } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + const client = createDiscordClient({ botToken: state.botToken }); + + try { + const messages = await client.getPinnedMessages(channelId); + return { + messages: messages.map((msg: any) => ({ + id: msg.id, + channel_id: msg.channel_id, + content: msg.content, + timestamp: msg.timestamp, + author: { + id: msg.author.id, + username: msg.author.username, + discriminator: msg.author.discriminator, + }, + })), + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to get pinned messages: ${message}`); + } + }, + }); + +/** + * Array of all message-related tools + */ +export const messageTools = [ + createSendMessageTool, + createEditMessageTool, + createDeleteMessageTool, + createGetChannelMessagesTool, + createGetMessageTool, + createAddReactionTool, + createRemoveReactionTool, + createGetMessageReactionsTool, + createPinMessageTool, + createUnpinMessageTool, + createGetPinnedMessagesTool, +]; diff --git a/discord-bot/server/tools/roles.ts b/discord-bot/server/tools/roles.ts new file mode 100644 index 00000000..21be5cd0 --- /dev/null +++ b/discord-bot/server/tools/roles.ts @@ -0,0 +1,201 @@ +/** + * MCP tools for Discord role operations + * + * This file implements tools for: + * - Creating roles + * - Editing roles + * - Deleting roles + * - Listing guild roles + */ + +import type { Env } from "../main.ts"; +import { createDiscordClient } from "./utils/discord-client.ts"; +import { createPrivateTool } from "@decocms/runtime/mastra"; +import { + createRoleInputSchema, + createRoleOutputSchema, + editRoleInputSchema, + editRoleOutputSchema, + deleteRoleInputSchema, + deleteRoleOutputSchema, + getGuildRolesInputSchema, + getGuildRolesOutputSchema, +} from "../lib/types.ts"; + +/** + * CREATE_ROLE - Create a new role in a guild + */ +export const createCreateRoleTool = (env: Env) => + createPrivateTool({ + id: "CREATE_ROLE", + description: + "Create a new role in a Discord server (guild). You can configure the role name, color, permissions, and display settings. Requires MANAGE_ROLES permission.", + inputSchema: createRoleInputSchema, + outputSchema: createRoleOutputSchema, + execute: async ({ context }) => { + const { + guildId, + name, + permissions, + color, + hoist = false, + mentionable = false, + } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + const client = createDiscordClient({ botToken: state.botToken }); + + const body: any = { + hoist, + mentionable, + }; + + if (name) body.name = name; + if (permissions) body.permissions = permissions; + if (color !== undefined) body.color = color; + + try { + const role = await client.createRole(guildId, body); + return { + id: role.id, + name: role.name, + color: role.color, + hoist: role.hoist, + position: role.position, + permissions: role.permissions, + managed: role.managed, + mentionable: role.mentionable, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to create role: ${message}`); + } + }, + }); + +/** + * EDIT_ROLE - Edit an existing role in a guild + */ +export const createEditRoleTool = (env: Env) => + createPrivateTool({ + id: "EDIT_ROLE", + description: + "Modify an existing role in a Discord server (guild). You can change the name, color, permissions, and display settings. Requires MANAGE_ROLES permission.", + inputSchema: editRoleInputSchema, + outputSchema: editRoleOutputSchema, + execute: async ({ context }) => { + const { guildId, roleId, name, permissions, color, hoist, mentionable } = + context; + const state = env.DECO_REQUEST_CONTEXT.state; + + const client = createDiscordClient({ botToken: state.botToken }); + + const body: any = {}; + + if (name) body.name = name; + if (permissions) body.permissions = permissions; + if (color !== undefined) body.color = color; + if (hoist !== undefined) body.hoist = hoist; + if (mentionable !== undefined) body.mentionable = mentionable; + + if (Object.keys(body).length === 0) { + throw new Error("At least one field must be provided to edit the role"); + } + + try { + const role = await client.editRole(guildId, roleId, body); + return { + id: role.id, + name: role.name, + color: role.color, + hoist: role.hoist, + position: role.position, + permissions: role.permissions, + managed: role.managed, + mentionable: role.mentionable, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to edit role: ${message}`); + } + }, + }); + +/** + * DELETE_ROLE - Delete a role from a guild + */ +export const createDeleteRoleTool = (env: Env) => + createPrivateTool({ + id: "DELETE_ROLE", + description: + "Delete a role from a Discord server (guild). This action is permanent and cannot be undone. Requires MANAGE_ROLES permission.", + inputSchema: deleteRoleInputSchema, + outputSchema: deleteRoleOutputSchema, + execute: async ({ context }) => { + const { guildId, roleId, reason } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + const client = createDiscordClient({ botToken: state.botToken }); + + const searchParams: Record = {}; + if (reason) searchParams.reason = reason; + + try { + await client.deleteRole(guildId, roleId, searchParams); + return { + success: true, + message: "Role deleted successfully", + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to delete role: ${message}`); + } + }, + }); + +/** + * GET_GUILD_ROLES - Get all roles in a guild + */ +export const createGetGuildRolesTool = (env: Env) => + createPrivateTool({ + id: "GET_GUILD_ROLES", + description: + "Fetch all roles from a Discord server (guild). Returns role information including names, colors, permissions, and position in the hierarchy.", + inputSchema: getGuildRolesInputSchema, + outputSchema: getGuildRolesOutputSchema, + execute: async ({ context }) => { + const { guildId } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + const client = createDiscordClient({ botToken: state.botToken }); + + try { + const roles = await client.getGuildRoles(guildId); + return { + roles: roles.map((role: any) => ({ + id: role.id, + name: role.name, + color: role.color, + hoist: role.hoist, + position: role.position, + permissions: role.permissions, + managed: role.managed, + mentionable: role.mentionable, + })), + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to get guild roles: ${message}`); + } + }, + }); + +/** + * Array of all role-related tools + */ +export const roleTools = [ + createCreateRoleTool, + createEditRoleTool, + createDeleteRoleTool, + createGetGuildRolesTool, +]; diff --git a/discord-bot/server/tools/threads.ts b/discord-bot/server/tools/threads.ts new file mode 100644 index 00000000..0a1a8e05 --- /dev/null +++ b/discord-bot/server/tools/threads.ts @@ -0,0 +1,224 @@ +/** + * MCP tools for Discord thread operations + * + * This file implements tools for: + * - Creating threads + * - Joining and leaving threads + * - Listing active and archived threads + */ + +import type { Env } from "../main.ts"; +import { createDiscordClient } from "./utils/discord-client.ts"; +import { createPrivateTool } from "@decocms/runtime/mastra"; +import { + createThreadInputSchema, + createThreadOutputSchema, + joinThreadInputSchema, + joinThreadOutputSchema, + leaveThreadInputSchema, + leaveThreadOutputSchema, + getActiveThreadsInputSchema, + getActiveThreadsOutputSchema, + getArchivedThreadsInputSchema, + getArchivedThreadsOutputSchema, +} from "../lib/types.ts"; + +/** + * CREATE_THREAD - Create a new thread in a channel + */ +export const createCreateThreadTool = (env: Env) => + createPrivateTool({ + id: "CREATE_THREAD", + description: + "Create a new thread in a Discord channel. Threads are temporary sub-channels for organized conversations. You can configure auto-archive duration and other settings.", + inputSchema: createThreadInputSchema, + outputSchema: createThreadOutputSchema, + execute: async ({ context }) => { + const { + channelId, + name, + autoArchiveDuration, + type, + invitable, + rateLimitPerUser, + } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + if (!name || name.length < 1 || name.length > 100) { + throw new Error("Thread name must be between 1 and 100 characters"); + } + + const client = createDiscordClient({ botToken: state.botToken }); + + const body: any = { + name, + }; + + if (autoArchiveDuration !== undefined) { + body.auto_archive_duration = autoArchiveDuration; + } + if (type !== undefined) body.type = type; + if (invitable !== undefined) body.invitable = invitable; + if (rateLimitPerUser !== undefined) { + body.rate_limit_per_user = rateLimitPerUser; + } + + try { + const thread = await client.createThread(channelId, body); + return { + id: thread.id, + type: thread.type, + name: thread.name, + guild_id: thread.guild_id, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to create thread: ${message}`); + } + }, + }); + +/** + * JOIN_THREAD - Join a thread + */ +export const createJoinThreadTool = (env: Env) => + createPrivateTool({ + id: "JOIN_THREAD", + description: + "Join a thread channel. The bot needs to join a thread to receive messages and participate in the conversation.", + inputSchema: joinThreadInputSchema, + outputSchema: joinThreadOutputSchema, + execute: async ({ context }) => { + const { channelId } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + const client = createDiscordClient({ botToken: state.botToken }); + + try { + await client.joinThread(channelId); + return { + success: true, + message: "Successfully joined thread", + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to join thread: ${message}`); + } + }, + }); + +/** + * LEAVE_THREAD - Leave a thread + */ +export const createLeaveThreadTool = (env: Env) => + createPrivateTool({ + id: "LEAVE_THREAD", + description: + "Leave a thread channel. After leaving, the bot will no longer receive messages from that thread.", + inputSchema: leaveThreadInputSchema, + outputSchema: leaveThreadOutputSchema, + execute: async ({ context }) => { + const { channelId } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + const client = createDiscordClient({ botToken: state.botToken }); + + try { + await client.leaveThread(channelId); + return { + success: true, + message: "Successfully left thread", + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to leave thread: ${message}`); + } + }, + }); + +/** + * GET_ACTIVE_THREADS - Get all active threads in a guild + */ +export const createGetActiveThreadsTool = (env: Env) => + createPrivateTool({ + id: "GET_ACTIVE_THREADS", + description: + "Fetch all active (non-archived) threads in a Discord server (guild). Returns threads that are currently open for conversation.", + inputSchema: getActiveThreadsInputSchema, + outputSchema: getActiveThreadsOutputSchema, + execute: async ({ context }) => { + const { guildId } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + const client = createDiscordClient({ botToken: state.botToken }); + + try { + const response = await client.getActiveThreads(guildId); + return { + threads: response.threads.map((thread: any) => ({ + id: thread.id, + type: thread.type, + name: thread.name, + guild_id: thread.guild_id, + })), + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to get active threads: ${message}`); + } + }, + }); + +/** + * GET_ARCHIVED_THREADS - Get archived threads from a channel + */ +export const createGetArchivedThreadsTool = (env: Env) => + createPrivateTool({ + id: "GET_ARCHIVED_THREADS", + description: + "Fetch archived threads from a Discord channel. Can retrieve either public or private archived threads. Archived threads are inactive but can be reopened.", + inputSchema: getArchivedThreadsInputSchema, + outputSchema: getArchivedThreadsOutputSchema, + execute: async ({ context }) => { + const { channelId, type, before, limit = 50 } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + const client = createDiscordClient({ botToken: state.botToken }); + + const searchParams: Record = { + limit: Math.min(Math.max(limit, 1), 100).toString(), + }; + + if (before) searchParams.before = before; + + try { + const response = await client.getArchivedThreads( + channelId, + type, + searchParams, + ); + return { + threads: response.threads.map((thread: any) => ({ + id: thread.id, + type: thread.type, + name: thread.name, + guild_id: thread.guild_id, + })), + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to get archived threads: ${message}`); + } + }, + }); + +/** + * Array of all thread-related tools + */ +export const threadTools = [ + createCreateThreadTool, + createJoinThreadTool, + createLeaveThreadTool, + createGetActiveThreadsTool, + createGetArchivedThreadsTool, +]; diff --git a/discord-bot/server/tools/utils/discord-client.ts b/discord-bot/server/tools/utils/discord-client.ts new file mode 100644 index 00000000..7876a5c1 --- /dev/null +++ b/discord-bot/server/tools/utils/discord-client.ts @@ -0,0 +1,257 @@ +/** + * HTTP client for interacting with the Discord API. + * + * Documentation: https://discord.com/developers/docs/intro + */ + +import { makeApiRequest } from "@decocms/mcps-shared/tools/utils/api-client"; + +export interface DiscordClientConfig { + botToken: string; +} + +const DISCORD_API_URL = "https://discord.com/api/v10"; + +/** + * Makes an authenticated request to the Discord API + */ +async function makeRequest( + config: DiscordClientConfig, + method: string, + endpoint: string, + body?: unknown, + searchParams?: Record, +): Promise { + let url = `${DISCORD_API_URL}${endpoint}`; + + if (searchParams) { + const params = new URLSearchParams(searchParams); + url += `?${params.toString()}`; + } + + const options: RequestInit = { + method, + headers: { + Authorization: `Bot ${config.botToken}`, + "Content-Type": "application/json", + }, + }; + + if (body && (method === "POST" || method === "PATCH" || method === "PUT")) { + options.body = JSON.stringify(body); + } + + // For DELETE requests that return void, handle separately + if (method === "DELETE" || method === "PUT") { + const response = await fetch(url, options); + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `Discord API error: ${response.status} ${response.statusText}\n${errorText}`, + ); + } + // If response has content, parse it; otherwise return void + const contentType = response.headers.get("content-type"); + if (contentType && contentType.includes("application/json")) { + return await response.json(); + } + return; + } + + return await makeApiRequest(url, options, "Discord"); +} + +/** + * Creates a Discord client with all available methods + */ +export function createDiscordClient(config: DiscordClientConfig) { + return { + // Messages + sendMessage: (channelId: string, body: unknown) => + makeRequest(config, "POST", `/channels/${channelId}/messages`, body), + + editMessage: (channelId: string, messageId: string, body: unknown) => + makeRequest( + config, + "PATCH", + `/channels/${channelId}/messages/${messageId}`, + body, + ), + + deleteMessage: (channelId: string, messageId: string) => + makeRequest( + config, + "DELETE", + `/channels/${channelId}/messages/${messageId}`, + ), + + getMessage: (channelId: string, messageId: string) => + makeRequest( + config, + "GET", + `/channels/${channelId}/messages/${messageId}`, + ), + + getChannelMessages: ( + channelId: string, + searchParams?: Record, + ) => + makeRequest( + config, + "GET", + `/channels/${channelId}/messages`, + undefined, + searchParams, + ), + + // Reactions + addReaction: (channelId: string, messageId: string, emoji: string) => + makeRequest( + config, + "PUT", + `/channels/${channelId}/messages/${messageId}/reactions/${emoji}/@me`, + ), + + removeReaction: (channelId: string, messageId: string, emoji: string) => + makeRequest( + config, + "DELETE", + `/channels/${channelId}/messages/${messageId}/reactions/${emoji}/@me`, + ), + + getMessageReactions: ( + channelId: string, + messageId: string, + emoji: string, + searchParams?: Record, + ) => + makeRequest( + config, + "GET", + `/channels/${channelId}/messages/${messageId}/reactions/${emoji}`, + undefined, + searchParams, + ), + + // Pins + pinMessage: (channelId: string, messageId: string) => + makeRequest(config, "PUT", `/channels/${channelId}/pins/${messageId}`), + + unpinMessage: (channelId: string, messageId: string) => + makeRequest(config, "DELETE", `/channels/${channelId}/pins/${messageId}`), + + getPinnedMessages: (channelId: string) => + makeRequest(config, "GET", `/channels/${channelId}/pins`), + + // Channels + createChannel: (guildId: string, body: unknown) => + makeRequest(config, "POST", `/guilds/${guildId}/channels`, body), + + getGuildChannels: (guildId: string) => + makeRequest(config, "GET", `/guilds/${guildId}/channels`), + + // Guilds + listBotGuilds: (searchParams?: Record) => + makeRequest(config, "GET", `/users/@me/guilds`, undefined, searchParams), + + getGuild: (guildId: string, searchParams?: Record) => + makeRequest(config, "GET", `/guilds/${guildId}`, undefined, searchParams), + + getGuildMembers: (guildId: string, searchParams?: Record) => + makeRequest( + config, + "GET", + `/guilds/${guildId}/members`, + undefined, + searchParams, + ), + + banMember: (guildId: string, userId: string, body?: unknown) => + makeRequest(config, "PUT", `/guilds/${guildId}/bans/${userId}`, body), + + // Roles + createRole: (guildId: string, body: unknown) => + makeRequest(config, "POST", `/guilds/${guildId}/roles`, body), + + editRole: (guildId: string, roleId: string, body: unknown) => + makeRequest(config, "PATCH", `/guilds/${guildId}/roles/${roleId}`, body), + + deleteRole: ( + guildId: string, + roleId: string, + searchParams?: Record, + ) => + makeRequest( + config, + "DELETE", + `/guilds/${guildId}/roles/${roleId}`, + undefined, + searchParams, + ), + + getGuildRoles: (guildId: string) => + makeRequest(config, "GET", `/guilds/${guildId}/roles`), + + // Threads + createThread: (channelId: string, body: unknown) => + makeRequest(config, "POST", `/channels/${channelId}/threads`, body), + + joinThread: (channelId: string) => + makeRequest(config, "PUT", `/channels/${channelId}/thread-members/@me`), + + leaveThread: (channelId: string) => + makeRequest( + config, + "DELETE", + `/channels/${channelId}/thread-members/@me`, + ), + + getActiveThreads: (guildId: string) => + makeRequest(config, "GET", `/guilds/${guildId}/threads/active`), + + getArchivedThreads: ( + channelId: string, + type: "public" | "private", + searchParams?: Record, + ) => + makeRequest( + config, + "GET", + `/channels/${channelId}/threads/archived/${type}`, + undefined, + searchParams, + ), + + // Webhooks + createWebhook: (channelId: string, body: unknown) => + makeRequest(config, "POST", `/channels/${channelId}/webhooks`, body), + + executeWebhook: ( + webhookId: string, + webhookToken: string, + body: unknown, + searchParams?: Record, + ) => + makeRequest( + config, + "POST", + `/webhooks/${webhookId}/${webhookToken}`, + body, + searchParams, + ), + + deleteWebhook: (webhookId: string, webhookToken: string) => + makeRequest(config, "DELETE", `/webhooks/${webhookId}/${webhookToken}`), + + listChannelWebhooks: (channelId: string) => + makeRequest(config, "GET", `/channels/${channelId}/webhooks`), + + listGuildWebhooks: (guildId: string) => + makeRequest(config, "GET", `/guilds/${guildId}/webhooks`), + + // Users + getCurrentUser: () => makeRequest(config, "GET", `/users/@me`), + + getUser: (userId: string) => makeRequest(config, "GET", `/users/${userId}`), + }; +} diff --git a/discord-bot/server/tools/webhooks.ts b/discord-bot/server/tools/webhooks.ts new file mode 100644 index 00000000..d6cab4e3 --- /dev/null +++ b/discord-bot/server/tools/webhooks.ts @@ -0,0 +1,240 @@ +/** + * MCP tools for Discord webhook operations + * + * This file implements tools for: + * - Creating webhooks + * - Executing webhooks + * - Deleting webhooks + * - Listing webhooks + */ + +import type { Env } from "../main.ts"; +import { createDiscordClient } from "./utils/discord-client.ts"; +import { createPrivateTool } from "@decocms/runtime/mastra"; +import { + createWebhookInputSchema, + createWebhookOutputSchema, + executeWebhookInputSchema, + executeWebhookOutputSchema, + deleteWebhookInputSchema, + deleteWebhookOutputSchema, + listWebhooksInputSchema, + listWebhooksOutputSchema, +} from "../lib/types.ts"; + +/** + * CREATE_WEBHOOK - Create a new webhook in a channel + */ +export const createCreateWebhookTool = (env: Env) => + createPrivateTool({ + id: "CREATE_WEBHOOK", + description: + "Create a new webhook for a Discord channel. Webhooks allow external services to post messages to Discord channels. Returns the webhook ID and token needed for execution.", + inputSchema: createWebhookInputSchema, + outputSchema: createWebhookOutputSchema, + execute: async ({ context }) => { + const { channelId, name, avatar } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + if (!name || name.length < 1 || name.length > 80) { + throw new Error("Webhook name must be between 1 and 80 characters"); + } + + const client = createDiscordClient({ botToken: state.botToken }); + + const body: any = { + name, + }; + + if (avatar) { + body.avatar = avatar; + } + + try { + const webhook = await client.createWebhook(channelId, body); + return { + id: webhook.id, + type: webhook.type, + name: webhook.name, + token: webhook.token, + channel_id: webhook.channel_id, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to create webhook: ${message}`); + } + }, + }); + +/** + * EXECUTE_WEBHOOK - Execute a webhook to send a message + */ +export const createExecuteWebhookTool = (env: Env) => + createPrivateTool({ + id: "EXECUTE_WEBHOOK", + description: + "Execute a webhook to send a message to a Discord channel. Webhooks can send messages with custom usernames, avatars, and embeds without requiring bot authentication.", + inputSchema: executeWebhookInputSchema, + outputSchema: executeWebhookOutputSchema, + execute: async ({ context }) => { + const { + webhookId, + webhookToken, + content, + username, + avatarUrl, + tts = false, + embeds, + threadName, + wait = false, + threadId, + } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + if (!content && (!embeds || embeds.length === 0)) { + throw new Error("Webhook content or embeds are required"); + } + + if (content && content.length > 2000) { + throw new Error("Webhook content cannot exceed 2000 characters"); + } + + const client = createDiscordClient({ botToken: state.botToken }); + + const body: any = { + tts, + }; + + if (content) body.content = content; + if (username) body.username = username; + if (avatarUrl) body.avatar_url = avatarUrl; + if (embeds && embeds.length > 0) body.embeds = embeds; + if (threadName) body.thread_name = threadName; + + const searchParams: Record = {}; + if (wait) searchParams.wait = "true"; + if (threadId) searchParams.thread_id = threadId; + + try { + const message = await client.executeWebhook( + webhookId, + webhookToken, + body, + searchParams, + ); + + // If wait is false, Discord returns 204 No Content + if (!message) { + return { + id: "", + channel_id: "", + content: content || "", + timestamp: new Date().toISOString(), + author: { + id: "", + username: username || "Webhook", + discriminator: "0000", + }, + }; + } + + return { + id: message.id, + channel_id: message.channel_id, + content: message.content, + timestamp: message.timestamp, + author: { + id: message.author.id, + username: message.author.username, + discriminator: message.author.discriminator, + }, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to execute webhook: ${message}`); + } + }, + }); + +/** + * DELETE_WEBHOOK - Delete a webhook + */ +export const createDeleteWebhookTool = (env: Env) => + createPrivateTool({ + id: "DELETE_WEBHOOK", + description: + "Delete a webhook permanently. This action cannot be undone. The webhook will no longer be able to send messages to the channel.", + inputSchema: deleteWebhookInputSchema, + outputSchema: deleteWebhookOutputSchema, + execute: async ({ context }) => { + const { webhookId, webhookToken } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + const client = createDiscordClient({ botToken: state.botToken }); + + try { + await client.deleteWebhook(webhookId, webhookToken); + return { + success: true, + message: "Webhook deleted successfully", + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to delete webhook: ${message}`); + } + }, + }); + +/** + * LIST_WEBHOOKS - List webhooks from a channel or guild + */ +export const createListWebhooksTool = (env: Env) => + createPrivateTool({ + id: "LIST_WEBHOOKS", + description: + "List all webhooks from a Discord channel or server (guild). Returns webhook information including IDs, names, and tokens. Either channelId or guildId must be provided.", + inputSchema: listWebhooksInputSchema, + outputSchema: listWebhooksOutputSchema, + execute: async ({ context }) => { + const { channelId, guildId } = context; + const state = env.DECO_REQUEST_CONTEXT.state; + + if (!channelId && !guildId) { + throw new Error("Either channelId or guildId must be provided"); + } + + const client = createDiscordClient({ botToken: state.botToken }); + + try { + let webhooks; + if (channelId) { + webhooks = await client.listChannelWebhooks(channelId); + } else { + webhooks = await client.listGuildWebhooks(guildId!); + } + + return { + webhooks: webhooks.map((webhook: any) => ({ + id: webhook.id, + type: webhook.type, + name: webhook.name, + token: webhook.token, + channel_id: webhook.channel_id, + })), + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to list webhooks: ${message}`); + } + }, + }); + +/** + * Array of all webhook-related tools + */ +export const webhookTools = [ + createCreateWebhookTool, + createExecuteWebhookTool, + createDeleteWebhookTool, + createListWebhooksTool, +]; diff --git a/discord-bot/shared/deco.gen.ts b/discord-bot/shared/deco.gen.ts new file mode 100644 index 00000000..5d4764f4 --- /dev/null +++ b/discord-bot/shared/deco.gen.ts @@ -0,0 +1,28 @@ +// Generated types - do not edit manually + +import { z } from "zod"; + +export type Mcp Promise>> = { + [K in keyof T]: (( + input: Parameters[0], + ) => Promise>>) & { + asTool: () => Promise<{ + inputSchema: z.ZodType[0]>; + outputSchema?: z.ZodType>>; + description: string; + id: string; + execute: ( + input: Parameters[0], + ) => Promise>>; + }>; + }; +}; + +export const StateSchema = z.object({}); + +export interface Env { + DECO_CHAT_WORKSPACE: string; + DECO_CHAT_API_JWT_PUBLIC_KEY: string; +} + +export const Scopes = {}; diff --git a/discord-bot/tsconfig.json b/discord-bot/tsconfig.json new file mode 100644 index 00000000..736bc2dd --- /dev/null +++ b/discord-bot/tsconfig.json @@ -0,0 +1,43 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "verbatimModuleSyntax": false, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "allowJs": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + + /* Path Aliases */ + "baseUrl": ".", + "paths": { + "shared/*": ["./shared/*"], + "server/*": ["./server/*"], + "worker/*": ["./worker/*"] + }, + + /* Types */ + "types": ["@cloudflare/workers-types"] + }, + "include": [ + "server", + "shared", + "vite.config.ts" + ] +} + diff --git a/discord-bot/vite.config.ts b/discord-bot/vite.config.ts new file mode 100644 index 00000000..0b54ccb8 --- /dev/null +++ b/discord-bot/vite.config.ts @@ -0,0 +1,29 @@ +import { defineConfig } from "vite"; +import { cloudflare } from "@cloudflare/vite-plugin"; +import deco from "@decocms/mcps-shared/vite-plugin"; + +const VITE_SERVER_ENVIRONMENT_NAME = "server"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + cloudflare({ + configPath: "wrangler.toml", + viteEnvironment: { + name: VITE_SERVER_ENVIRONMENT_NAME, + }, + }), + deco(), + ], + + define: { + // Ensure proper module definitions for Cloudflare Workers context + "process.env.NODE_ENV": JSON.stringify( + process.env.NODE_ENV || "development", + ), + global: "globalThis", + }, + + // Clear cache more aggressively + cacheDir: "node_modules/.vite", +}); diff --git a/discord-bot/wrangler.toml b/discord-bot/wrangler.toml new file mode 100644 index 00000000..cdf98e0e --- /dev/null +++ b/discord-bot/wrangler.toml @@ -0,0 +1,17 @@ +#:schema node_modules/@decocms/runtime/config-schema.json +name = "discord-bot" +main = "server/main.ts" +compatibility_date = "2025-06-17" +compatibility_flags = [ "nodejs_compat" ] +scope = "deco" + +[deco] +workspace = "deco" +enable_workflows = false +local = false + +[deco.integration] +description = "Discord Bot integration for sending messages, managing channels, roles, threads, webhooks, and server moderation. Full control of your Discord server through MCP tools." +icon = "https://support.discord.com/hc/user_images/PRywUXcqg0v5DD6s7C3LyQ.jpeg" +friendlyName = "Discord teste" + diff --git a/package.json b/package.json index 96b34d63..2327f678 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "apify", "data-for-seo", "datajud", + "discord-bot", "gemini-pro-vision", "meta-ads", "nanobanana", From db7ac2158f1e718295097ccf6a032bb0ca3985de Mon Sep 17 00:00:00 2001 From: viniciusventura29 Date: Mon, 17 Nov 2025 22:21:21 -0300 Subject: [PATCH 2/3] refactor: update Discord bot tools and types - Removed outdated comments and documentation from the guilds.ts file to streamline the codebase. - Renamed tools for fetching user information to include "Discord" in their identifiers for clarity. - Updated the Env interface to include new methods for managing Discord interactions, enhancing the bot's capabilities. - Added comprehensive TypeScript types and Zod schemas for input/output validation in the deco.gen.ts file, improving data integrity and consistency across the Discord bot functionalities. --- discord-bot/server/tools/guilds.ts | 22 +- discord-bot/shared/deco.gen.ts | 2088 ++++++++++++++++++++++++++++ 2 files changed, 2094 insertions(+), 16 deletions(-) diff --git a/discord-bot/server/tools/guilds.ts b/discord-bot/server/tools/guilds.ts index 36128194..8e0b48d7 100644 --- a/discord-bot/server/tools/guilds.ts +++ b/discord-bot/server/tools/guilds.ts @@ -1,13 +1,3 @@ -/** - * MCP tools for Discord guild (server) operations - * - * This file implements tools for: - * - Listing bot guilds - * - Getting guild information - * - Managing guild members - * - User operations - */ - import type { Env } from "../main.ts"; import { createDiscordClient } from "./utils/discord-client.ts"; import { createPrivateTool } from "@decocms/runtime/mastra"; @@ -193,9 +183,9 @@ export const createBanMemberTool = (env: Env) => /** * GET_CURRENT_USER - Get information about the bot's user account */ -export const createGetCurrentUserTool = (env: Env) => +export const createGetCurrentDiscordUserTool = (env: Env) => createPrivateTool({ - id: "GET_CURRENT_USER", + id: "GET_CURRENT_DISCORD_USER", description: "Get information about the bot's own user account including username, discriminator, ID, and bot status.", inputSchema: getCurrentUserInputSchema, @@ -223,9 +213,9 @@ export const createGetCurrentUserTool = (env: Env) => /** * GET_USER - Get information about a specific user */ -export const createGetUserTool = (env: Env) => +export const createGetDiscordUserTool = (env: Env) => createPrivateTool({ - id: "GET_USER", + id: "GET_DISCORD_USER", description: "Get information about a specific Discord user by their user ID. Returns username, discriminator, and public profile information.", inputSchema: getUserInputSchema, @@ -259,6 +249,6 @@ export const guildTools = [ createGetGuildTool, createGetGuildMembersTool, createBanMemberTool, - createGetCurrentUserTool, - createGetUserTool, + createGetCurrentDiscordUserTool, + createGetDiscordUserTool, ]; diff --git a/discord-bot/shared/deco.gen.ts b/discord-bot/shared/deco.gen.ts index 5d4764f4..433c0307 100644 --- a/discord-bot/shared/deco.gen.ts +++ b/discord-bot/shared/deco.gen.ts @@ -1,5 +1,1907 @@ // Generated types - do not edit manually +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do canal Discord + */ +export type String = string; +/** + * ID da mensagem + */ +export type String_1 = string; +/** + * Emoji para adicionar (Unicode ou custom emoji ID) + */ +export type String_2 = string; + +export interface ADD_REACTIONInput { + channelId: String; + messageId: String_1; + emoji: String_2; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type Boolean = boolean; +export type String_3 = string; + +export interface ADD_REACTIONOutput { + success: Boolean; + message: String_3; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do servidor Discord + */ +export type String_4 = string; +/** + * ID do usuário a ser banido + */ +export type String_5 = string; +/** + * Número de dias de mensagens a deletar (0-7) + */ +export type Number = number; +/** + * Razão do banimento + */ +export type String_6 = string; + +export interface BAN_MEMBERInput { + guildId: String_4; + userId: String_5; + deleteMessageDays?: Number; + reason?: String_6; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type Boolean_1 = boolean; +export type String_7 = string; + +export interface BAN_MEMBEROutput { + success: Boolean_1; + message: String_7; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do servidor Discord + */ +export type String_8 = string; +/** + * Nome do canal (2-100 caracteres) + */ +export type String_9 = string; +/** + * Tipo do canal (0=Text, 2=Voice, 4=Category, 5=News) + */ +export type Number_1 = number; +/** + * Tópico do canal + */ +export type String_10 = string; +/** + * Se o canal é NSFW + */ +export type Boolean_2 = boolean; +/** + * ID da categoria pai + */ +export type String_11 = string; +/** + * Posição do canal na lista + */ +export type Number_2 = number; +/** + * Taxa de bits do canal de voz + */ +export type Number_3 = number; +/** + * Limite de usuários do canal de voz + */ +export type Number_4 = number; +/** + * Taxa de limite de mensagens por usuário + */ +export type Number_5 = number; + +export interface CREATE_CHANNELInput { + guildId: String_8; + name: String_9; + type?: Number_1; + topic?: String_10; + nsfw?: Boolean_2; + parentId?: String_11; + position?: Number_2; + bitrate?: Number_3; + userLimit?: Number_4; + rateLimitPerUser?: Number_5; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type String_12 = string; +export type Number_6 = number; +export type String_13 = string; +export type String_14 = string; + +export interface CREATE_CHANNELOutput { + id: String_12; + type: Number_6; + name?: String_13; + guild_id?: String_14; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do servidor Discord + */ +export type String_15 = string; +/** + * Nome da role + */ +export type String_16 = string; +/** + * Permissões da role (bitfield) + */ +export type String_17 = string; +/** + * Cor da role (número RGB) + */ +export type Number_7 = number; +/** + * Se a role aparece separadamente na lista de membros + */ +export type Boolean_3 = boolean; +/** + * Se a role pode ser mencionada + */ +export type Boolean_4 = boolean; + +export interface CREATE_ROLEInput { + guildId: String_15; + name?: String_16; + permissions?: String_17; + color?: Number_7; + hoist?: Boolean_3; + mentionable?: Boolean_4; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type String_18 = string; +export type String_19 = string; +export type Number_8 = number; +export type Boolean_5 = boolean; +export type Number_9 = number; +export type String_20 = string; +export type Boolean_6 = boolean; +export type Boolean_7 = boolean; + +export interface CREATE_ROLEOutput { + id: String_18; + name: String_19; + color: Number_8; + hoist: Boolean_5; + position: Number_9; + permissions: String_20; + managed: Boolean_6; + mentionable: Boolean_7; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do canal Discord + */ +export type String_21 = string; +/** + * Nome da thread + */ +export type String_22 = string; +/** + * Duração para arquivamento automático em minutos + */ +export type Number_10 = number; +/** + * Tipo da thread + */ +export type Number_11 = number; +/** + * Se membros podem convidar outros + */ +export type Boolean_8 = boolean; +/** + * Taxa de limite de mensagens por usuário + */ +export type Number_12 = number; + +export interface CREATE_THREADInput { + channelId: String_21; + name: String_22; + autoArchiveDuration?: Number_10; + type?: Number_11; + invitable?: Boolean_8; + rateLimitPerUser?: Number_12; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type String_23 = string; +export type Number_13 = number; +export type String_24 = string; +export type String_25 = string; + +export interface CREATE_THREADOutput { + id: String_23; + type: Number_13; + name?: String_24; + guild_id?: String_25; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do canal Discord + */ +export type String_26 = string; +/** + * Nome do webhook + */ +export type String_27 = string; +/** + * URL ou data URI da imagem do avatar + */ +export type String_28 = string; + +export interface CREATE_WEBHOOKInput { + channelId: String_26; + name: String_27; + avatar?: String_28; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type String_29 = string; +export type Number_14 = number; +export type String_30 = string; +export type Null = null; +export type String_31 = string; +export type String_32 = string; + +export interface CREATE_WEBHOOKOutput { + id: String_29; + type: Number_14; + name?: + | ( + | { + [k: string]: unknown; + } + | String_30 + ) + | Null; + token?: String_31; + channel_id: String_32; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type String_33 = string; + +export interface DECO_CHAT_OAUTH_STARTInput { + returnUrl: String_33; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type String_34 = string; +export type Array = String_34[]; + +export interface DECO_CHAT_OAUTH_STARTOutput { + stateSchema?: unknown; + scopes?: Array; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export interface DECO_CHAT_STATE_VALIDATIONInput { + state?: unknown; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type Boolean_9 = boolean; + +export interface DECO_CHAT_STATE_VALIDATIONOutput { + valid: Boolean_9; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export interface DECO_CHAT_VIEWS_LISTInput {} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type String_35 = string; +export type String_36 = string; +export type String_37 = string; +export type String_38 = string; +export type String_39 = string; +export type String_40 = string; +export type String_41 = string; +export type String_42 = string; +export type String_43 = string; +export type Array_2 = String_43[]; +export type String_44 = string; +export type String_45 = "none" | "open" | "autoPin"; +export type Array_1 = Object[]; + +export interface DECO_CHAT_VIEWS_LISTOutput { + views: Array_1; +} +export interface Object { + id?: String_35; + name?: String_36; + title: String_37; + description?: String_38; + icon: String_39; + url?: String_40; + mimeTypePattern?: String_41; + resourceName?: String_42; + tools?: Array_2; + prompt?: String_44; + installBehavior?: String_45; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do canal Discord + */ +export type String_46 = string; +/** + * ID da mensagem a ser deletada + */ +export type String_47 = string; + +export interface DELETE_MESSAGEInput { + channelId: String_46; + messageId: String_47; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type Boolean_10 = boolean; +export type String_48 = string; + +export interface DELETE_MESSAGEOutput { + success: Boolean_10; + message: String_48; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do servidor Discord + */ +export type String_49 = string; +/** + * ID da role a ser deletada + */ +export type String_50 = string; +/** + * Razão da exclusão + */ +export type String_51 = string; + +export interface DELETE_ROLEInput { + guildId: String_49; + roleId: String_50; + reason?: String_51; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type Boolean_11 = boolean; +export type String_52 = string; + +export interface DELETE_ROLEOutput { + success: Boolean_11; + message: String_52; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do webhook + */ +export type String_53 = string; +/** + * Token do webhook + */ +export type String_54 = string; + +export interface DELETE_WEBHOOKInput { + webhookId: String_53; + webhookToken: String_54; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type Boolean_12 = boolean; +export type String_55 = string; + +export interface DELETE_WEBHOOKOutput { + success: Boolean_12; + message: String_55; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do canal Discord + */ +export type String_56 = string; +/** + * ID da mensagem a ser editada + */ +export type String_57 = string; +/** + * Novo conteúdo da mensagem + */ +export type String_58 = string; +export type String_59 = string; +export type String_60 = string; +export type String_61 = string; +export type String_62 = string; +export type Number_15 = number; +export type String_63 = string; +export type String_64 = string; +export type String_65 = string; +export type String_66 = string; +export type String_67 = string; +export type String_68 = string; +export type String_69 = string; +export type String_70 = string; +export type String_71 = string; +export type Boolean_13 = boolean; +export type Array_4 = Object_6[]; +/** + * Novos embeds da mensagem + */ +export type Array_3 = Object_1[]; + +export interface EDIT_MESSAGEInput { + channelId: String_56; + messageId: String_57; + content?: String_58; + embeds?: Array_3; +} +export interface Object_1 { + title?: String_59; + description?: String_60; + url?: String_61; + timestamp?: String_62; + color?: Number_15; + footer?: Object_2; + image?: Object_3; + thumbnail?: Object_4; + author?: Object_5; + fields?: Array_4; +} +export interface Object_2 { + text: String_63; + icon_url?: String_64; +} +export interface Object_3 { + url: String_65; +} +export interface Object_4 { + url: String_66; +} +export interface Object_5 { + name: String_67; + url?: String_68; + icon_url?: String_69; +} +export interface Object_6 { + name: String_70; + value: String_71; + inline?: Boolean_13; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type String_72 = string; +export type String_73 = string; +export type String_74 = string; +export type String_75 = string; +export type String_76 = string; +export type String_77 = string; +export type String_78 = string; + +export interface EDIT_MESSAGEOutput { + id: String_72; + channel_id: String_73; + content: String_74; + timestamp: String_75; + author: Object_7; +} +export interface Object_7 { + id: String_76; + username: String_77; + discriminator: String_78; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do servidor Discord + */ +export type String_79 = string; +/** + * ID da role a ser editada + */ +export type String_80 = string; +/** + * Novo nome da role + */ +export type String_81 = string; +/** + * Novas permissões da role + */ +export type String_82 = string; +/** + * Nova cor da role + */ +export type Number_16 = number; +/** + * Se a role aparece separadamente + */ +export type Boolean_14 = boolean; +/** + * Se a role pode ser mencionada + */ +export type Boolean_15 = boolean; + +export interface EDIT_ROLEInput { + guildId: String_79; + roleId: String_80; + name?: String_81; + permissions?: String_82; + color?: Number_16; + hoist?: Boolean_14; + mentionable?: Boolean_15; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type String_83 = string; +export type String_84 = string; +export type Number_17 = number; +export type Boolean_16 = boolean; +export type Number_18 = number; +export type String_85 = string; +export type Boolean_17 = boolean; +export type Boolean_18 = boolean; + +export interface EDIT_ROLEOutput { + id: String_83; + name: String_84; + color: Number_17; + hoist: Boolean_16; + position: Number_18; + permissions: String_85; + managed: Boolean_17; + mentionable: Boolean_18; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do webhook + */ +export type String_86 = string; +/** + * Token do webhook + */ +export type String_87 = string; +/** + * Conteúdo da mensagem + */ +export type String_88 = string; +/** + * Nome de usuário customizado + */ +export type String_89 = string; +/** + * URL do avatar customizado + */ +export type String_90 = string; +/** + * Se é uma mensagem de texto para fala + */ +export type Boolean_19 = boolean; +export type String_91 = string; +export type String_92 = string; +export type String_93 = string; +export type String_94 = string; +export type Number_19 = number; +export type String_95 = string; +export type String_96 = string; +export type String_97 = string; +export type String_98 = string; +export type String_99 = string; +export type String_100 = string; +export type String_101 = string; +export type String_102 = string; +export type String_103 = string; +export type Boolean_20 = boolean; +export type Array_6 = Object_13[]; +/** + * Lista de embeds + */ +export type Array_5 = Object_8[]; +/** + * Nome da thread a criar (para webhooks de fórum) + */ +export type String_104 = string; +/** + * Se deve aguardar e retornar a mensagem criada + */ +export type Boolean_21 = boolean; +/** + * ID da thread onde enviar a mensagem + */ +export type String_105 = string; + +export interface EXECUTE_WEBHOOKInput { + webhookId: String_86; + webhookToken: String_87; + content?: String_88; + username?: String_89; + avatarUrl?: String_90; + tts?: Boolean_19; + embeds?: Array_5; + threadName?: String_104; + wait?: Boolean_21; + threadId?: String_105; +} +export interface Object_8 { + title?: String_91; + description?: String_92; + url?: String_93; + timestamp?: String_94; + color?: Number_19; + footer?: Object_9; + image?: Object_10; + thumbnail?: Object_11; + author?: Object_12; + fields?: Array_6; +} +export interface Object_9 { + text: String_95; + icon_url?: String_96; +} +export interface Object_10 { + url: String_97; +} +export interface Object_11 { + url: String_98; +} +export interface Object_12 { + name: String_99; + url?: String_100; + icon_url?: String_101; +} +export interface Object_13 { + name: String_102; + value: String_103; + inline?: Boolean_20; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type String_106 = string; +export type String_107 = string; +export type String_108 = string; +export type String_109 = string; +export type String_110 = string; +export type String_111 = string; +export type String_112 = string; + +export interface EXECUTE_WEBHOOKOutput { + id: String_106; + channel_id: String_107; + content: String_108; + timestamp: String_109; + author: Object_14; +} +export interface Object_14 { + id: String_110; + username: String_111; + discriminator: String_112; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do servidor Discord + */ +export type String_113 = string; + +export interface GET_ACTIVE_THREADSInput { + guildId: String_113; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type String_114 = string; +export type Number_20 = number; +export type String_115 = string; +export type String_116 = string; +export type Array_7 = Object_15[]; + +export interface GET_ACTIVE_THREADSOutput { + threads: Array_7; +} +export interface Object_15 { + id: String_114; + type: Number_20; + name?: String_115; + guild_id?: String_116; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do canal Discord + */ +export type String_117 = string; +/** + * Tipo de threads arquivadas + */ +export type String_118 = "public" | "private"; +/** + * Timestamp ISO8601 - retornar threads arquivadas antes desta data + */ +export type String_119 = string; +/** + * Número máximo de threads a retornar + */ +export type Number_21 = number; + +export interface GET_ARCHIVED_THREADSInput { + channelId: String_117; + type: String_118; + before?: String_119; + limit?: Number_21; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type String_120 = string; +export type Number_22 = number; +export type String_121 = string; +export type String_122 = string; +export type Array_8 = Object_16[]; + +export interface GET_ARCHIVED_THREADSOutput { + threads: Array_8; +} +export interface Object_16 { + id: String_120; + type: Number_22; + name?: String_121; + guild_id?: String_122; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do canal Discord + */ +export type String_123 = string; +/** + * Número máximo de mensagens a retornar (1-100) + */ +export type Number_23 = number; +/** + * ID da mensagem - buscar mensagens antes desta + */ +export type String_124 = string; +/** + * ID da mensagem - buscar mensagens após esta + */ +export type String_125 = string; +/** + * ID da mensagem - buscar mensagens ao redor desta + */ +export type String_126 = string; + +export interface GET_CHANNEL_MESSAGESInput { + channelId: String_123; + limit?: Number_23; + before?: String_124; + after?: String_125; + around?: String_126; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type String_127 = string; +export type String_128 = string; +export type String_129 = string; +export type String_130 = string; +export type String_131 = string; +export type String_132 = string; +export type String_133 = string; +export type Array_9 = Object_17[]; + +export interface GET_CHANNEL_MESSAGESOutput { + messages: Array_9; +} +export interface Object_17 { + id: String_127; + channel_id: String_128; + content: String_129; + timestamp: String_130; + author: Object_18; +} +export interface Object_18 { + id: String_131; + username: String_132; + discriminator: String_133; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export interface GET_CURRENT_DISCORD_USERInput {} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type String_134 = string; +export type String_135 = string; +export type String_136 = string; +export type Boolean_22 = boolean; + +export interface GET_CURRENT_DISCORD_USEROutput { + id: String_134; + username: String_135; + discriminator: String_136; + bot?: Boolean_22; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do usuário + */ +export type String_137 = string; + +export interface GET_DISCORD_USERInput { + userId: String_137; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type String_138 = string; +export type String_139 = string; +export type String_140 = string; +export type Boolean_23 = boolean; + +export interface GET_DISCORD_USEROutput { + id: String_138; + username: String_139; + discriminator: String_140; + bot?: Boolean_23; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do servidor Discord + */ +export type String_141 = string; +/** + * Se deve incluir contagem de membros + */ +export type Boolean_24 = boolean; + +export interface GET_GUILDInput { + guildId: String_141; + withCounts?: Boolean_24; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type String_142 = string; +export type String_143 = string; +export type String_144 = string; +export type Null_1 = null; +export type String_145 = string; +export type String_146 = string; +export type Number_24 = number; + +export interface GET_GUILDOutput { + id: String_142; + name: String_143; + icon?: + | ( + | { + [k: string]: unknown; + } + | String_144 + ) + | Null_1; + owner_id: String_145; + permissions?: String_146; + member_count?: Number_24; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do servidor Discord + */ +export type String_147 = string; + +export interface GET_GUILD_CHANNELSInput { + guildId: String_147; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type String_148 = string; +export type Number_25 = number; +export type String_149 = string; +export type String_150 = string; +export type Array_10 = Object_19[]; + +export interface GET_GUILD_CHANNELSOutput { + channels: Array_10; +} +export interface Object_19 { + id: String_148; + type: Number_25; + name?: String_149; + guild_id?: String_150; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do servidor Discord + */ +export type String_151 = string; +/** + * Número máximo de membros a retornar + */ +export type Number_26 = number; +/** + * ID do usuário - buscar membros após este + */ +export type String_152 = string; + +export interface GET_GUILD_MEMBERSInput { + guildId: String_151; + limit?: Number_26; + after?: String_152; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type String_153 = string; +export type String_154 = string; +export type String_155 = string; +export type String_156 = string; +export type Null_2 = null; +export type String_157 = string; +export type Array_12 = String_157[]; +export type String_158 = string; +export type Array_11 = Object_20[]; + +export interface GET_GUILD_MEMBERSOutput { + members: Array_11; +} +export interface Object_20 { + user?: Object_21; + nick?: + | ( + | { + [k: string]: unknown; + } + | String_156 + ) + | Null_2; + roles: Array_12; + joined_at: String_158; +} +export interface Object_21 { + id: String_153; + username: String_154; + discriminator: String_155; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do servidor Discord + */ +export type String_159 = string; + +export interface GET_GUILD_ROLESInput { + guildId: String_159; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type String_160 = string; +export type String_161 = string; +export type Number_27 = number; +export type Boolean_25 = boolean; +export type Number_28 = number; +export type String_162 = string; +export type Boolean_26 = boolean; +export type Boolean_27 = boolean; +export type Array_13 = Object_22[]; + +export interface GET_GUILD_ROLESOutput { + roles: Array_13; +} +export interface Object_22 { + id: String_160; + name: String_161; + color: Number_27; + hoist: Boolean_25; + position: Number_28; + permissions: String_162; + managed: Boolean_26; + mentionable: Boolean_27; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do canal Discord + */ +export type String_163 = string; +/** + * ID da mensagem + */ +export type String_164 = string; + +export interface GET_MESSAGEInput { + channelId: String_163; + messageId: String_164; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type String_165 = string; +export type String_166 = string; +export type String_167 = string; +export type String_168 = string; +export type String_169 = string; +export type String_170 = string; +export type String_171 = string; + +export interface GET_MESSAGEOutput { + id: String_165; + channel_id: String_166; + content: String_167; + timestamp: String_168; + author: Object_23; +} +export interface Object_23 { + id: String_169; + username: String_170; + discriminator: String_171; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do canal Discord + */ +export type String_172 = string; +/** + * ID da mensagem + */ +export type String_173 = string; +/** + * Emoji para buscar reações + */ +export type String_174 = string; +/** + * Número máximo de usuários a retornar + */ +export type Number_29 = number; +/** + * ID do usuário - buscar usuários após este + */ +export type String_175 = string; + +export interface GET_MESSAGE_REACTIONSInput { + channelId: String_172; + messageId: String_173; + emoji: String_174; + limit?: Number_29; + after?: String_175; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type String_176 = string; +export type String_177 = string; +export type String_178 = string; +export type Array_14 = Object_24[]; + +export interface GET_MESSAGE_REACTIONSOutput { + users: Array_14; +} +export interface Object_24 { + id: String_176; + username: String_177; + discriminator: String_178; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do canal Discord + */ +export type String_179 = string; + +export interface GET_PINNED_MESSAGESInput { + channelId: String_179; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type String_180 = string; +export type String_181 = string; +export type String_182 = string; +export type String_183 = string; +export type String_184 = string; +export type String_185 = string; +export type String_186 = string; +export type Array_15 = Object_25[]; + +export interface GET_PINNED_MESSAGESOutput { + messages: Array_15; +} +export interface Object_25 { + id: String_180; + channel_id: String_181; + content: String_182; + timestamp: String_183; + author: Object_26; +} +export interface Object_26 { + id: String_184; + username: String_185; + discriminator: String_186; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export interface GET_USERInput {} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type String_187 = string; +export type StringNull = String_188 | Null_3; +export type String_188 = string; +export type Null_3 = null; +export type StringNull_1 = String_189 | Null_4; +export type String_189 = string; +export type Null_4 = null; +export type String_190 = string; + +export interface GET_USEROutput { + id: String_187; + name: StringNull; + avatar: StringNull_1; + email: String_190; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID da thread + */ +export type String_191 = string; + +export interface JOIN_THREADInput { + channelId: String_191; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type Boolean_28 = boolean; +export type String_192 = string; + +export interface JOIN_THREADOutput { + success: Boolean_28; + message: String_192; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID da thread + */ +export type String_193 = string; + +export interface LEAVE_THREADInput { + channelId: String_193; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type Boolean_29 = boolean; +export type String_194 = string; + +export interface LEAVE_THREADOutput { + success: Boolean_29; + message: String_194; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * Número máximo de servidores a retornar + */ +export type Number_30 = number; +/** + * ID do servidor - retornar servidores antes deste + */ +export type String_195 = string; +/** + * ID do servidor - retornar servidores após este + */ +export type String_196 = string; +/** + * Se deve incluir contagem aproximada de membros + */ +export type Boolean_30 = boolean; + +export interface LIST_BOT_GUILDSInput { + limit?: Number_30; + before?: String_195; + after?: String_196; + withCounts?: Boolean_30; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type String_197 = string; +export type String_198 = string; +export type String_199 = string; +export type Null_5 = null; +export type Boolean_31 = boolean; +export type String_200 = string; +export type Array_16 = Object_27[]; + +export interface LIST_BOT_GUILDSOutput { + guilds: Array_16; +} +export interface Object_27 { + id: String_197; + name: String_198; + icon?: + | ( + | { + [k: string]: unknown; + } + | String_199 + ) + | Null_5; + owner?: Boolean_31; + permissions?: String_200; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do canal Discord (se omitido, requer guildId) + */ +export type String_201 = string; +/** + * ID do servidor Discord (se omitido, requer channelId) + */ +export type String_202 = string; + +export interface LIST_WEBHOOKSInput { + channelId?: String_201; + guildId?: String_202; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type String_203 = string; +export type Number_31 = number; +export type String_204 = string; +export type Null_6 = null; +export type String_205 = string; +export type String_206 = string; +export type Array_17 = Object_28[]; + +export interface LIST_WEBHOOKSOutput { + webhooks: Array_17; +} +export interface Object_28 { + id: String_203; + type: Number_31; + name?: + | ( + | { + [k: string]: unknown; + } + | String_204 + ) + | Null_6; + token?: String_205; + channel_id: String_206; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do canal Discord + */ +export type String_207 = string; +/** + * ID da mensagem a ser fixada + */ +export type String_208 = string; + +export interface PIN_MESSAGEInput { + channelId: String_207; + messageId: String_208; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type Boolean_32 = boolean; +export type String_209 = string; + +export interface PIN_MESSAGEOutput { + success: Boolean_32; + message: String_209; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do canal Discord + */ +export type String_210 = string; +/** + * ID da mensagem + */ +export type String_211 = string; +/** + * Emoji para adicionar (Unicode ou custom emoji ID) + */ +export type String_212 = string; + +export interface REMOVE_REACTIONInput { + channelId: String_210; + messageId: String_211; + emoji: String_212; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type Boolean_33 = boolean; +export type String_213 = string; + +export interface REMOVE_REACTIONOutput { + success: Boolean_33; + message: String_213; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do canal Discord onde enviar a mensagem + */ +export type String_214 = string; +/** + * Conteúdo da mensagem (máximo 2000 caracteres) + */ +export type String_215 = string; +/** + * Enviar como texto para fala + */ +export type Boolean_34 = boolean; +export type String_216 = string; +export type String_217 = string; +export type String_218 = string; +export type String_219 = string; +export type Number_32 = number; +export type String_220 = string; +export type String_221 = string; +export type String_222 = string; +export type String_223 = string; +export type String_224 = string; +export type String_225 = string; +export type String_226 = string; +export type String_227 = string; +export type String_228 = string; +export type Boolean_35 = boolean; +export type Array_19 = Object_34[]; +/** + * Lista de embeds para incluir na mensagem + */ +export type Array_18 = Object_29[]; +/** + * ID da mensagem para responder + */ +export type String_229 = string; +/** + * Se deve mencionar o autor da mensagem original na resposta + */ +export type Boolean_36 = boolean; + +export interface SEND_MESSAGEInput { + channelId: String_214; + content?: String_215; + tts?: Boolean_34; + embeds?: Array_18; + replyToMessageId?: String_229; + replyMention?: Boolean_36; +} +export interface Object_29 { + title?: String_216; + description?: String_217; + url?: String_218; + timestamp?: String_219; + color?: Number_32; + footer?: Object_30; + image?: Object_31; + thumbnail?: Object_32; + author?: Object_33; + fields?: Array_19; +} +export interface Object_30 { + text: String_220; + icon_url?: String_221; +} +export interface Object_31 { + url: String_222; +} +export interface Object_32 { + url: String_223; +} +export interface Object_33 { + name: String_224; + url?: String_225; + icon_url?: String_226; +} +export interface Object_34 { + name: String_227; + value: String_228; + inline?: Boolean_35; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type String_230 = string; +export type String_231 = string; +export type String_232 = string; +export type String_233 = string; +export type String_234 = string; +export type String_235 = string; +export type String_236 = string; + +export interface SEND_MESSAGEOutput { + id: String_230; + channel_id: String_231; + content: String_232; + timestamp: String_233; + author: Object_35; +} +export interface Object_35 { + id: String_234; + username: String_235; + discriminator: String_236; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * ID do canal Discord + */ +export type String_237 = string; +/** + * ID da mensagem a ser fixada + */ +export type String_238 = string; + +export interface UNPIN_MESSAGEInput { + channelId: String_237; + messageId: String_238; +} + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type Boolean_37 = boolean; +export type String_239 = string; + +export interface UNPIN_MESSAGEOutput { + success: Boolean_37; + message: String_239; +} + import { z } from "zod"; export type Mcp Promise>> = { @@ -23,6 +1925,192 @@ export const StateSchema = z.object({}); export interface Env { DECO_CHAT_WORKSPACE: string; DECO_CHAT_API_JWT_PUBLIC_KEY: string; + SELF: Mcp<{ + /** + * Add a reaction (emoji) to a Discord message. Supports both Unicode emojis and custom server emojis. For custom emojis, use the format 'name:id'. + */ + ADD_REACTION: (input: ADD_REACTIONInput) => Promise; + /** + * Ban a user from a Discord server (guild). Can optionally delete messages from the banned user from the last 0-7 days. Requires BAN_MEMBERS permission. + */ + BAN_MEMBER: (input: BAN_MEMBERInput) => Promise; + /** + * Create a new channel in a Discord server (guild). Supports text channels, voice channels, categories, and announcement channels. You can configure permissions, position, and various channel settings. + */ + CREATE_CHANNEL: ( + input: CREATE_CHANNELInput, + ) => Promise; + /** + * Create a new role in a Discord server (guild). You can configure the role name, color, permissions, and display settings. Requires MANAGE_ROLES permission. + */ + CREATE_ROLE: (input: CREATE_ROLEInput) => Promise; + /** + * Create a new thread in a Discord channel. Threads are temporary sub-channels for organized conversations. You can configure auto-archive duration and other settings. + */ + CREATE_THREAD: (input: CREATE_THREADInput) => Promise; + /** + * Create a new webhook for a Discord channel. Webhooks allow external services to post messages to Discord channels. Returns the webhook ID and token needed for execution. + */ + CREATE_WEBHOOK: ( + input: CREATE_WEBHOOKInput, + ) => Promise; + /** + * OAuth for Deco Chat + */ + DECO_CHAT_OAUTH_START: ( + input: DECO_CHAT_OAUTH_STARTInput, + ) => Promise; + /** + * Validate the state of the OAuth flow + */ + DECO_CHAT_STATE_VALIDATION: ( + input: DECO_CHAT_STATE_VALIDATIONInput, + ) => Promise; + /** + * List views exposed by this MCP + */ + DECO_CHAT_VIEWS_LIST: ( + input: DECO_CHAT_VIEWS_LISTInput, + ) => Promise; + /** + * Delete a message from a Discord channel. The bot needs Manage Messages permission to delete messages from other users, or can always delete its own messages. + */ + DELETE_MESSAGE: ( + input: DELETE_MESSAGEInput, + ) => Promise; + /** + * Delete a role from a Discord server (guild). This action is permanent and cannot be undone. Requires MANAGE_ROLES permission. + */ + DELETE_ROLE: (input: DELETE_ROLEInput) => Promise; + /** + * Delete a webhook permanently. This action cannot be undone. The webhook will no longer be able to send messages to the channel. + */ + DELETE_WEBHOOK: ( + input: DELETE_WEBHOOKInput, + ) => Promise; + /** + * Edit an existing message in a Discord channel. You can update the content and/or embeds. Only messages sent by the bot can be edited. + */ + EDIT_MESSAGE: (input: EDIT_MESSAGEInput) => Promise; + /** + * Modify an existing role in a Discord server (guild). You can change the name, color, permissions, and display settings. Requires MANAGE_ROLES permission. + */ + EDIT_ROLE: (input: EDIT_ROLEInput) => Promise; + /** + * Execute a webhook to send a message to a Discord channel. Webhooks can send messages with custom usernames, avatars, and embeds without requiring bot authentication. + */ + EXECUTE_WEBHOOK: ( + input: EXECUTE_WEBHOOKInput, + ) => Promise; + /** + * Fetch all active (non-archived) threads in a Discord server (guild). Returns threads that are currently open for conversation. + */ + GET_ACTIVE_THREADS: ( + input: GET_ACTIVE_THREADSInput, + ) => Promise; + /** + * Fetch archived threads from a Discord channel. Can retrieve either public or private archived threads. Archived threads are inactive but can be reopened. + */ + GET_ARCHIVED_THREADS: ( + input: GET_ARCHIVED_THREADSInput, + ) => Promise; + /** + * Fetch messages from a Discord channel. Supports pagination with before, after, and around parameters. Returns up to 100 messages per request. + */ + GET_CHANNEL_MESSAGES: ( + input: GET_CHANNEL_MESSAGESInput, + ) => Promise; + /** + * Get information about the bot's own user account including username, discriminator, ID, and bot status. + */ + GET_CURRENT_DISCORD_USER: ( + input: GET_CURRENT_DISCORD_USERInput, + ) => Promise; + /** + * Get information about a specific Discord user by their user ID. Returns username, discriminator, and public profile information. + */ + GET_DISCORD_USER: ( + input: GET_DISCORD_USERInput, + ) => Promise; + /** + * Fetch detailed information about a specific Discord server (guild) including name, icon, owner, and member counts. Requires the bot to be a member of the guild. + */ + GET_GUILD: (input: GET_GUILDInput) => Promise; + /** + * Fetch all channels from a Discord server (guild). Returns a list of all channels including text, voice, categories, and announcement channels with their metadata. + */ + GET_GUILD_CHANNELS: ( + input: GET_GUILD_CHANNELSInput, + ) => Promise; + /** + * Fetch members from a Discord server (guild). Returns user information, nicknames, roles, and join dates. Supports pagination with up to 1000 members per request. Requires the GUILD_MEMBERS privileged intent. + */ + GET_GUILD_MEMBERS: ( + input: GET_GUILD_MEMBERSInput, + ) => Promise; + /** + * Fetch all roles from a Discord server (guild). Returns role information including names, colors, permissions, and position in the hierarchy. + */ + GET_GUILD_ROLES: ( + input: GET_GUILD_ROLESInput, + ) => Promise; + /** + * Fetch a specific message by ID from a Discord channel. Returns the full message object with all metadata. + */ + GET_MESSAGE: (input: GET_MESSAGEInput) => Promise; + /** + * Get a list of users who reacted to a message with a specific emoji. Returns up to 100 users per request with pagination support. + */ + GET_MESSAGE_REACTIONS: ( + input: GET_MESSAGE_REACTIONSInput, + ) => Promise; + /** + * Fetch all pinned messages from a Discord channel. Returns a list of up to 50 pinned messages. + */ + GET_PINNED_MESSAGES: ( + input: GET_PINNED_MESSAGESInput, + ) => Promise; + /** + * Get the current logged in user + */ + GET_USER: (input: GET_USERInput) => Promise; + /** + * Join a thread channel. The bot needs to join a thread to receive messages and participate in the conversation. + */ + JOIN_THREAD: (input: JOIN_THREADInput) => Promise; + /** + * Leave a thread channel. After leaving, the bot will no longer receive messages from that thread. + */ + LEAVE_THREAD: (input: LEAVE_THREADInput) => Promise; + /** + * List all Discord servers (guilds) where the bot is currently a member. Returns guild names, IDs, icons, and permissions. Supports pagination. + */ + LIST_BOT_GUILDS: ( + input: LIST_BOT_GUILDSInput, + ) => Promise; + /** + * List all webhooks from a Discord channel or server (guild). Returns webhook information including IDs, names, and tokens. Either channelId or guildId must be provided. + */ + LIST_WEBHOOKS: (input: LIST_WEBHOOKSInput) => Promise; + /** + * Pin a message in a Discord channel. Pinned messages appear at the top of the channel. Channels can have up to 50 pinned messages. + */ + PIN_MESSAGE: (input: PIN_MESSAGEInput) => Promise; + /** + * Remove the bot's reaction from a Discord message. Only removes reactions added by the bot itself. + */ + REMOVE_REACTION: ( + input: REMOVE_REACTIONInput, + ) => Promise; + /** + * Send a message to a Discord channel. Supports text content, embeds, TTS, and replies. Messages can include rich formatting and mentions. + */ + SEND_MESSAGE: (input: SEND_MESSAGEInput) => Promise; + /** + * Unpin a previously pinned message from a Discord channel. Removes the message from the pinned messages list. + */ + UNPIN_MESSAGE: (input: UNPIN_MESSAGEInput) => Promise; + }>; } export const Scopes = {}; From 43a4606b53562d853e79c56a89f145ace5373105 Mon Sep 17 00:00:00 2001 From: JonasJesus Date: Fri, 21 Nov 2025 10:42:44 -0300 Subject: [PATCH 3/3] feat: add Discord webhook handling and command registration --- bun.lock | 3 + discord-bot/package.json | 1 + discord-bot/scripts/register-commands.ts | 328 ++++++++++++++++++++++ discord-bot/server/lib/types.ts | 100 +++++++ discord-bot/server/lib/verification.ts | 95 +++++++ discord-bot/server/lib/webhook-handler.ts | 327 +++++++++++++++++++++ discord-bot/server/main.ts | 25 +- 7 files changed, 877 insertions(+), 2 deletions(-) create mode 100644 discord-bot/scripts/register-commands.ts create mode 100644 discord-bot/server/lib/verification.ts create mode 100644 discord-bot/server/lib/webhook-handler.ts diff --git a/bun.lock b/bun.lock index 6434a428..fad38087 100644 --- a/bun.lock +++ b/bun.lock @@ -99,6 +99,7 @@ "version": "1.0.0", "dependencies": { "@decocms/runtime": "0.24.0", + "tweetnacl": "^1.0.3", "zod": "^3.24.3", }, "devDependencies": { @@ -2250,6 +2251,8 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tweetnacl": ["tweetnacl@1.0.3", "", {}, "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="], + "type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], diff --git a/discord-bot/package.json b/discord-bot/package.json index 158bae15..096332e4 100644 --- a/discord-bot/package.json +++ b/discord-bot/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@decocms/runtime": "0.24.0", + "tweetnacl": "^1.0.3", "zod": "^3.24.3" }, "devDependencies": { diff --git a/discord-bot/scripts/register-commands.ts b/discord-bot/scripts/register-commands.ts new file mode 100644 index 00000000..459e8e7e --- /dev/null +++ b/discord-bot/scripts/register-commands.ts @@ -0,0 +1,328 @@ +/** + * Script para registrar comandos slash no Discord + * + * Uso: + * bun run scripts/register-commands.ts + * + * Requisitos: + * - Defina as variáveis de ambiente: + * - DISCORD_APP_ID: ID da aplicação Discord + * - DISCORD_BOT_TOKEN: Token do bot + * - DISCORD_GUILD_ID (opcional): ID do servidor para comandos de teste + */ + +const DISCORD_API_URL = "https://discord.com/api/v10"; + +// ======================================== +// Configuração dos Comandos +// ======================================== + +const commands = [ + { + name: "deco", + description: "Interagir com agente Deco via Discord", + options: [ + { + name: "message", + description: "Mensagem para enviar ao agente", + type: 3, // STRING + required: false, + }, + ], + }, + { + name: "ping", + description: "Verificar se o bot está online", + }, + { + name: "help", + description: "Mostrar ajuda sobre comandos disponíveis", + }, +]; + +// ======================================== +// Funções de Registro +// ======================================== + +async function registerGlobalCommands( + appId: string, + botToken: string +): Promise { + console.log("📝 Registrando comandos globais..."); + console.log(`Total: ${commands.length} comandos`); + + const url = `${DISCORD_API_URL}/applications/${appId}/commands`; + + try { + const response = await fetch(url, { + method: "PUT", + headers: { + "Authorization": `Bot ${botToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(commands), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error( + `Falha ao registrar comandos: ${response.status} - ${error}` + ); + } + + const result = await response.json(); + console.log("✅ Comandos globais registrados com sucesso!"); + console.log(`Registrados ${result.length} comandos`); + + result.forEach((cmd: any) => { + console.log(` - /${cmd.name}: ${cmd.description}`); + }); + + console.log( + "\n⚠️ Nota: Comandos globais podem demorar até 1 hora para propagar." + ); + } catch (error) { + console.error("❌ Erro ao registrar comandos globais:", error); + throw error; + } +} + +async function registerGuildCommands( + appId: string, + botToken: string, + guildId: string +): Promise { + console.log(`📝 Registrando comandos no servidor ${guildId}...`); + console.log(`Total: ${commands.length} comandos`); + + const url = + `${DISCORD_API_URL}/applications/${appId}/guilds/${guildId}/commands`; + + try { + const response = await fetch(url, { + method: "PUT", + headers: { + "Authorization": `Bot ${botToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(commands), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error( + `Falha ao registrar comandos: ${response.status} - ${error}` + ); + } + + const result = await response.json(); + console.log("✅ Comandos do servidor registrados com sucesso!"); + console.log(`Registrados ${result.length} comandos`); + + result.forEach((cmd: any) => { + console.log(` - /${cmd.name}: ${cmd.description}`); + }); + + console.log("\n✅ Comandos devem estar disponíveis imediatamente!"); + } catch (error) { + console.error("❌ Erro ao registrar comandos do servidor:", error); + throw error; + } +} + +async function listCommands( + appId: string, + botToken: string, + guildId?: string +): Promise { + const scope = guildId ? `servidor ${guildId}` : "globais"; + console.log(`📋 Listando comandos ${scope}...`); + + const url = guildId + ? `${DISCORD_API_URL}/applications/${appId}/guilds/${guildId}/commands` + : `${DISCORD_API_URL}/applications/${appId}/commands`; + + try { + const response = await fetch(url, { + headers: { + "Authorization": `Bot ${botToken}`, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Falha ao listar comandos: ${response.status} - ${error}`); + } + + const result = await response.json(); + + if (result.length === 0) { + console.log(`ℹ️ Nenhum comando ${scope} encontrado.`); + return; + } + + console.log(`\nComandos ${scope} (${result.length}):`); + result.forEach((cmd: any) => { + console.log(`\n 📌 /${cmd.name}`); + console.log(` ID: ${cmd.id}`); + console.log(` Descrição: ${cmd.description}`); + if (cmd.options && cmd.options.length > 0) { + console.log(` Opções:`); + cmd.options.forEach((opt: any) => { + const required = opt.required ? " (obrigatório)" : ""; + console.log(` - ${opt.name}: ${opt.description}${required}`); + }); + } + }); + } catch (error) { + console.error("❌ Erro ao listar comandos:", error); + throw error; + } +} + +async function deleteAllCommands( + appId: string, + botToken: string, + guildId?: string +): Promise { + const scope = guildId ? `servidor ${guildId}` : "globais"; + console.log(`🗑️ Deletando todos os comandos ${scope}...`); + + const url = guildId + ? `${DISCORD_API_URL}/applications/${appId}/guilds/${guildId}/commands` + : `${DISCORD_API_URL}/applications/${appId}/commands`; + + try { + // Primeiro, listar comandos existentes + const listResponse = await fetch(url, { + headers: { + "Authorization": `Bot ${botToken}`, + }, + }); + + if (!listResponse.ok) { + throw new Error(`Falha ao listar comandos: ${listResponse.status}`); + } + + const existingCommands = await listResponse.json(); + + if (existingCommands.length === 0) { + console.log(`ℹ️ Nenhum comando ${scope} para deletar.`); + return; + } + + console.log(`Encontrados ${existingCommands.length} comandos para deletar`); + + // Deletar todos de uma vez enviando array vazio + const deleteResponse = await fetch(url, { + method: "PUT", + headers: { + "Authorization": `Bot ${botToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify([]), + }); + + if (!deleteResponse.ok) { + const error = await deleteResponse.text(); + throw new Error(`Falha ao deletar comandos: ${deleteResponse.status} - ${error}`); + } + + console.log(`✅ Todos os comandos ${scope} foram deletados!`); + } catch (error) { + console.error("❌ Erro ao deletar comandos:", error); + throw error; + } +} + +// ======================================== +// CLI +// ======================================== + +async function main() { + const appId = process.env.DISCORD_APP_ID; + const botToken = process.env.DISCORD_BOT_TOKEN; + const guildId = process.env.DISCORD_GUILD_ID; + + if (!appId || !botToken) { + console.error("❌ Erro: Variáveis de ambiente não configuradas!"); + console.error("\nDefina as seguintes variáveis:"); + console.error(" - DISCORD_APP_ID: ID da aplicação Discord"); + console.error(" - DISCORD_BOT_TOKEN: Token do bot"); + console.error( + " - DISCORD_GUILD_ID (opcional): ID do servidor para comandos de teste" + ); + console.error("\nExemplo:"); + console.error( + " DISCORD_APP_ID=123456 DISCORD_BOT_TOKEN=abc123 bun run scripts/register-commands.ts" + ); + process.exit(1); + } + + const action = process.argv[2] || "register"; + + console.log("🤖 Discord Bot - Gerenciador de Comandos\n"); + + try { + switch (action) { + case "register": + case "reg": + if (guildId) { + console.log("🎯 Modo: Registrar comandos no servidor (instantâneo)"); + await registerGuildCommands(appId, botToken, guildId); + } else { + console.log("🌍 Modo: Registrar comandos globais (demora até 1h)"); + await registerGlobalCommands(appId, botToken); + } + break; + + case "list": + case "ls": + await listCommands(appId, botToken, guildId); + break; + + case "delete": + case "del": + await deleteAllCommands(appId, botToken, guildId); + break; + + case "help": + case "--help": + case "-h": + console.log("Uso: bun run scripts/register-commands.ts [ação]\n"); + console.log("Ações disponíveis:"); + console.log(" register, reg Registrar comandos (padrão)"); + console.log(" list, ls Listar comandos existentes"); + console.log(" delete, del Deletar todos os comandos"); + console.log(" help Mostrar esta ajuda"); + console.log("\nVariáveis de ambiente:"); + console.log(" DISCORD_APP_ID ID da aplicação (obrigatório)"); + console.log(" DISCORD_BOT_TOKEN Token do bot (obrigatório)"); + console.log(" DISCORD_GUILD_ID ID do servidor (opcional)"); + console.log("\nExemplos:"); + console.log(" # Registrar globalmente"); + console.log(" DISCORD_APP_ID=123 DISCORD_BOT_TOKEN=abc bun run scripts/register-commands.ts"); + console.log("\n # Registrar em servidor específico"); + console.log(" DISCORD_APP_ID=123 DISCORD_BOT_TOKEN=abc DISCORD_GUILD_ID=456 bun run scripts/register-commands.ts"); + console.log("\n # Listar comandos"); + console.log(" DISCORD_APP_ID=123 DISCORD_BOT_TOKEN=abc bun run scripts/register-commands.ts list"); + break; + + default: + console.error(`❌ Ação desconhecida: ${action}`); + console.error("Use 'help' para ver as ações disponíveis."); + process.exit(1); + } + + console.log("\n✨ Concluído!"); + } catch (error) { + console.error("\n💥 Erro fatal:", error); + process.exit(1); + } +} + +// Executar apenas se for o arquivo principal +if (import.meta.main) { + main(); +} + diff --git a/discord-bot/server/lib/types.ts b/discord-bot/server/lib/types.ts index aaded78a..08560e89 100644 --- a/discord-bot/server/lib/types.ts +++ b/discord-bot/server/lib/types.ts @@ -1157,3 +1157,103 @@ export const listWebhooksInputSchema = z.object({ export const listWebhooksOutputSchema = z.object({ webhooks: z.array(createWebhookOutputSchema), }); + +// ======================================== +// Discord Interactions (Webhooks) +// ======================================== + +/** + * Tipos de interação do Discord + */ +export enum InteractionType { + PING = 1, + APPLICATION_COMMAND = 2, + MESSAGE_COMPONENT = 3, + APPLICATION_COMMAND_AUTOCOMPLETE = 4, + MODAL_SUBMIT = 5, +} + +/** + * Tipos de resposta de interação + */ +export enum InteractionResponseType { + PONG = 1, + CHANNEL_MESSAGE_WITH_SOURCE = 4, + DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE = 5, + DEFERRED_UPDATE_MESSAGE = 6, + UPDATE_MESSAGE = 7, + APPLICATION_COMMAND_AUTOCOMPLETE_RESULT = 8, + MODAL = 9, +} + +/** + * Dados da interação de comando + */ +export interface InteractionDataOption { + name: string; + type: number; + value?: string | number | boolean; + options?: InteractionDataOption[]; + focused?: boolean; +} + +export interface InteractionData { + id: string; + name: string; + type: number; + resolved?: any; + options?: InteractionDataOption[]; + guild_id?: string; + target_id?: string; + custom_id?: string; + component_type?: number; + values?: string[]; + components?: DiscordComponent[]; +} + +/** + * Estrutura principal da interação recebida do Discord + */ +export interface DiscordInteraction { + id: string; + application_id: string; + type: InteractionType; + data?: InteractionData; + guild_id?: string; + channel_id?: string; + member?: DiscordGuildMember; + user?: DiscordUser; + token: string; + version: number; + message?: DiscordMessage; + locale?: string; + guild_locale?: string; + app_permissions?: string; +} + +/** + * Resposta de interação + */ +export interface InteractionResponse { + type: InteractionResponseType; + data?: InteractionCallbackData; +} + +export interface InteractionCallbackData { + tts?: boolean; + content?: string; + embeds?: DiscordEmbed[]; + allowed_mentions?: AllowedMentions; + flags?: number; + components?: DiscordMessageComponent[]; + attachments?: DiscordMessageAttachment[]; +} + +/** + * Payload recebido via webhook + */ +export interface WebhookPayload { + body: string; + signature: string | null; + timestamp: string | null; +} diff --git a/discord-bot/server/lib/verification.ts b/discord-bot/server/lib/verification.ts new file mode 100644 index 00000000..34720233 --- /dev/null +++ b/discord-bot/server/lib/verification.ts @@ -0,0 +1,95 @@ +/** + * Verificação de assinaturas de webhooks do Discord + * Usa o algoritmo Ed25519 para validar que as requisições vêm do Discord + */ +import nacl from "tweetnacl"; + +/** + * Converte string hexadecimal para Uint8Array + */ +function hexToUint8Array(hex: string): Uint8Array { + if (hex.length % 2 !== 0) { + throw new Error("Hex string inválido: comprimento ímpar"); + } + + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16); + } + return bytes; +} + +/** + * Verifica a assinatura Ed25519 de uma requisição do Discord + * + * @param body - Corpo da requisição em formato de string (não parseado) + * @param signature - Header X-Signature-Ed25519 + * @param timestamp - Header X-Signature-Timestamp + * @param publicKey - Chave pública do Discord (obtida no Developer Portal) + * @returns true se a assinatura é válida, false caso contrário + */ +export async function verifyDiscordSignature( + body: string, + signature: string | null, + timestamp: string | null, + publicKey: string +): Promise { + if (!signature || !timestamp) { + console.error("Missing signature or timestamp headers"); + return false; + } + + try { + // Validar timestamp (rejeitar requisições muito antigas - > 5 minutos) + const now = Math.floor(Date.now() / 1000); + const requestTime = parseInt(timestamp, 10); + + if (isNaN(requestTime)) { + console.error("Invalid timestamp format"); + return false; + } + + const timeDiff = Math.abs(now - requestTime); + if (timeDiff > 300) { // 5 minutos + console.warn(`Request timestamp too old: ${timeDiff}s difference`); + return false; + } + + // Construir a mensagem que o Discord assinou + const message = timestamp + body; + + // Converter de hexadecimal para bytes + const signatureBytes = hexToUint8Array(signature); + const publicKeyBytes = hexToUint8Array(publicKey); + const messageBytes = new TextEncoder().encode(message); + + // Verificar assinatura usando Ed25519 + const isValid = nacl.sign.detached.verify( + messageBytes, + signatureBytes, + publicKeyBytes + ); + + if (!isValid) { + console.error("Invalid signature"); + } + + return isValid; + } catch (error) { + console.error("Error verifying Discord signature:", error); + return false; + } +} + +/** + * Extrai os headers de assinatura de uma requisição + */ +export function extractSignatureHeaders( + request: Request +): { signature: string | null; timestamp: string | null } { + return { + signature: request.headers.get("X-Signature-Ed25519"), + timestamp: request.headers.get("X-Signature-Timestamp"), + }; +} + diff --git a/discord-bot/server/lib/webhook-handler.ts b/discord-bot/server/lib/webhook-handler.ts new file mode 100644 index 00000000..c555f112 --- /dev/null +++ b/discord-bot/server/lib/webhook-handler.ts @@ -0,0 +1,327 @@ +/** + * Handler para webhooks do Discord + * Processa interações recebidas via webhook + */ +import type { Env } from "../main.ts"; +import { + type DiscordInteraction, + InteractionType, + InteractionResponseType, + type InteractionResponse, +} from "./types.ts"; +import { + verifyDiscordSignature, + extractSignatureHeaders, +} from "./verification.ts"; +import { DISCORD_API_URL } from "./constants.ts"; + +/** + * Handler principal para requisições de webhook do Discord + */ +export async function handleDiscordWebhook( + request: Request, + env: Env +): Promise { + // 1. Ler o corpo da requisição (precisa ser string para validação) + const body = await request.text(); + + // 2. Extrair headers de assinatura + const { signature, timestamp } = extractSignatureHeaders(request); + + // 3. Validar assinatura + const state = env.DECO_REQUEST_CONTEXT?.state; + + // Suporte para dev local e produção + const publicKey = state?.discordPublicKey || (env as any).DISCORD_PUBLIC_KEY; + + if (!publicKey) { + console.error("Discord Public Key not configured"); + return new Response("Server configuration error", { status: 500 }); + } + + const isValid = await verifyDiscordSignature( + body, + signature, + timestamp, + publicKey + ); + + if (!isValid) { + console.error("Invalid Discord signature"); + return new Response("Invalid signature", { status: 401 }); + } + + // 4. Parsear a interação + let interaction: DiscordInteraction; + try { + interaction = JSON.parse(body) as DiscordInteraction; + } catch (error) { + console.error("Failed to parse interaction:", error); + return new Response("Invalid JSON", { status: 400 }); + } + + // 5. Processar com base no tipo de interação + return await processInteraction(interaction, env); +} + +/** + * Processa diferentes tipos de interação + */ +async function processInteraction( + interaction: DiscordInteraction, + env: Env +): Promise { + switch (interaction.type) { + case InteractionType.PING: + // Discord está validando o endpoint + return respondToPing(); + + case InteractionType.APPLICATION_COMMAND: + // Comando de aplicação (slash command) + return await handleApplicationCommand(interaction, env); + + case InteractionType.MESSAGE_COMPONENT: + // Interação com componente (botão, select menu, etc) + return await handleMessageComponent(interaction, env); + + case InteractionType.APPLICATION_COMMAND_AUTOCOMPLETE: + // Autocomplete de comando + return await handleAutocomplete(interaction, env); + + case InteractionType.MODAL_SUBMIT: + // Submissão de modal + return await handleModalSubmit(interaction, env); + + default: + console.warn(`Unknown interaction type: ${interaction.type}`); + return new Response("Unknown interaction type", { status: 400 }); + } +} + +/** + * Responde ao PING do Discord (validação de endpoint) + */ +function respondToPing(): Response { + const response: InteractionResponse = { + type: InteractionResponseType.PONG, + }; + + return new Response(JSON.stringify(response), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +} + +/** + * Processa comandos de aplicação (slash commands) + */ +async function handleApplicationCommand( + interaction: DiscordInteraction, + env: Env +): Promise { + // Responder imediatamente para evitar timeout (3 segundos) + const response: InteractionResponse = { + type: InteractionResponseType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE, + }; + + // Processar comando de forma assíncrona + // @ts-ignore - waitUntil existe em Cloudflare Workers + if (env.ctx?.waitUntil) { + // @ts-ignore + env.ctx.waitUntil(processCommandAsync(interaction, env)); + } else { + // Fallback para ambientes sem waitUntil + processCommandAsync(interaction, env).catch((error) => { + console.error("Error processing command:", error); + }); + } + + return new Response(JSON.stringify(response), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +} + +/** + * Processa o comando de forma assíncrona + */ +async function processCommandAsync( + interaction: DiscordInteraction, + env: Env +): Promise { + try { + const commandName = interaction.data?.name; + const options = interaction.data?.options || []; + + // Extrair valor do primeiro argumento (se houver) + const firstOption = options[0]; + const userInput = firstOption?.value?.toString() || ""; + + console.log(`Processing command: ${commandName}`, { + guildId: interaction.guild_id, + channelId: interaction.channel_id, + userId: interaction.member?.user?.id || interaction.user?.id, + input: userInput, + }); + + // Aqui você pode: + // 1. Buscar configuração do comando em um KV ou D1 + // 2. Rotear para um agente Deco específico + // 3. Processar com IA + // 4. Integrar com outros serviços + + // Por enquanto, vamos responder com uma mensagem simples + const responseMessage = `Comando **/${commandName}** recebido!\n\n` + + `📝 Input: ${userInput || "(nenhum)"}\n` + + `🆔 Guild: ${interaction.guild_id}\n` + + `📍 Channel: ${interaction.channel_id}`; + + // Enviar resposta via followup + await sendFollowupMessage(interaction, env, { + content: responseMessage, + }); + } catch (error) { + console.error("Error in processCommandAsync:", error); + + // Enviar mensagem de erro + await sendFollowupMessage(interaction, env, { + content: "❌ Ocorreu um erro ao processar o comando.", + }); + } +} + +/** + * Envia uma mensagem de followup (após resposta inicial) + */ +async function sendFollowupMessage( + interaction: DiscordInteraction, + env: Env, + message: { + content?: string; + embeds?: any[]; + components?: any[]; + } +): Promise { + const url = `${DISCORD_API_URL}/webhooks/${interaction.application_id}/${interaction.token}`; + + // Suporte para dev local e produção + const state = env.DECO_REQUEST_CONTEXT?.state; + const botToken = state?.botToken || (env as any).DISCORD_BOT_TOKEN; + + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bot ${botToken}`, + }, + body: JSON.stringify(message), + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error("Failed to send followup message:", response.status, errorText); + throw new Error(`Failed to send followup: ${response.status}`); + } +} + +/** + * Processa interações com componentes (botões, select menus) + */ +async function handleMessageComponent( + interaction: DiscordInteraction, + _env: Env +): Promise { + // Responder ao componente + const response: InteractionResponse = { + type: InteractionResponseType.UPDATE_MESSAGE, + data: { + content: `Você clicou no componente: ${interaction.data?.custom_id}`, + }, + }; + + return new Response(JSON.stringify(response), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +} + +/** + * Processa autocomplete de comandos + */ +async function handleAutocomplete( + _interaction: DiscordInteraction, + _env: Env +): Promise { + // Retornar opções de autocomplete + const response: InteractionResponse = { + type: InteractionResponseType.APPLICATION_COMMAND_AUTOCOMPLETE_RESULT, + data: { + // @ts-ignore - choices não está em InteractionCallbackData mas é válido + choices: [ + { name: "Opção 1", value: "opcao1" }, + { name: "Opção 2", value: "opcao2" }, + ], + }, + }; + + return new Response(JSON.stringify(response), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +} + +/** + * Processa submissão de modals + */ +async function handleModalSubmit( + _interaction: DiscordInteraction, + _env: Env +): Promise { + // Processar dados do modal + const response: InteractionResponse = { + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "Modal recebido com sucesso!", + }, + }; + + return new Response(JSON.stringify(response), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +} + +/** + * Edita a resposta original de uma interação + */ +export async function editOriginalResponse( + interaction: DiscordInteraction, + env: Env, + message: { + content?: string; + embeds?: any[]; + components?: any[]; + } +): Promise { + const url = `${DISCORD_API_URL}/webhooks/${interaction.application_id}/${interaction.token}/messages/@original`; + + // Suporte para dev local e produção + const state = env.DECO_REQUEST_CONTEXT?.state; + const botToken = state?.botToken || (env as any).DISCORD_BOT_TOKEN; + + const response = await fetch(url, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + Authorization: `Bot ${botToken}`, + }, + body: JSON.stringify(message), + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error("Failed to edit original response:", response.status, errorText); + throw new Error(`Failed to edit response: ${response.status}`); + } +} + diff --git a/discord-bot/server/main.ts b/discord-bot/server/main.ts index f81f4007..1b3faf9a 100644 --- a/discord-bot/server/main.ts +++ b/discord-bot/server/main.ts @@ -10,6 +10,7 @@ import { } from "../shared/deco.gen.ts"; import { tools } from "./tools/index.ts"; +import { handleDiscordWebhook } from "./lib/webhook-handler.ts"; /** * State schema for Discord Bot MCP configuration. @@ -21,6 +22,11 @@ export const StateSchema = BaseStateSchema.extend({ .describe( "Discord Bot Token from Developer Portal (https://discord.com/developers/applications)", ), + discordPublicKey: z + .string() + .describe( + "Discord Application Public Key from Developer Portal (required for webhook interactions)", + ), }); /** @@ -54,9 +60,24 @@ const runtime = withRuntime({ }, tools, /** - * Fallback directly to assets for all requests that do not match a tool or auth. + * Custom fetch handler to handle webhooks and fallback to assets */ - fetch: (req: Request, env: Env) => env.ASSETS.fetch(req), + fetch: async (req: Request, env: Env) => { + const url = new URL(req.url); + + // Rota para receber webhooks do Discord + if (url.pathname === "/discord/webhook" && req.method === "POST") { + return await handleDiscordWebhook(req, env); + } + + // Fallback para assets (se disponível) + if (env.ASSETS) { + return env.ASSETS.fetch(req); + } + + // Resposta padrão se ASSETS não estiver disponível + return new Response("Not Found", { status: 404 }); + }, }); export default runtime;