From 1ab71d158289ab9a310812df8c593f3dfaecaed7 Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 10 Dec 2025 08:12:44 +0000 Subject: [PATCH] feat: Add Discord MCP integration and improve README - Add comprehensive Discord MCP app with OAuth2 authentication - Implement 6 Discord tools: send messages, list guilds/channels, get messages, add reactions, get user info - Add proper TypeScript types for Discord API - Update README with Discord integration documentation - Improve README wording for better user experience - Update tool count to 165+ tools across 12 applications - Add Discord to example use cases - Fix LICENSE file link in README Co-authored-by: openhands --- README.md | 34 +- src/app/mcp/apps/discord/common.ts | 9 + src/app/mcp/apps/discord/discord-client.ts | 211 +++++++++ src/app/mcp/apps/discord/index.ts | 17 + .../mcp/apps/discord/tools/add-reaction.ts | 63 +++ .../mcp/apps/discord/tools/get-messages.ts | 89 ++++ .../mcp/apps/discord/tools/get-user-info.ts | 58 +++ src/app/mcp/apps/discord/tools/index.ts | 15 + .../mcp/apps/discord/tools/list-channels.ts | 70 +++ src/app/mcp/apps/discord/tools/list-guilds.ts | 49 ++ .../mcp/apps/discord/tools/send-message.ts | 110 +++++ src/app/mcp/apps/discord/types.ts | 431 ++++++++++++++++++ src/app/mcp/apps/index.ts | 2 + 13 files changed, 1140 insertions(+), 18 deletions(-) create mode 100644 src/app/mcp/apps/discord/common.ts create mode 100644 src/app/mcp/apps/discord/discord-client.ts create mode 100644 src/app/mcp/apps/discord/index.ts create mode 100644 src/app/mcp/apps/discord/tools/add-reaction.ts create mode 100644 src/app/mcp/apps/discord/tools/get-messages.ts create mode 100644 src/app/mcp/apps/discord/tools/get-user-info.ts create mode 100644 src/app/mcp/apps/discord/tools/index.ts create mode 100644 src/app/mcp/apps/discord/tools/list-channels.ts create mode 100644 src/app/mcp/apps/discord/tools/list-guilds.ts create mode 100644 src/app/mcp/apps/discord/tools/send-message.ts create mode 100644 src/app/mcp/apps/discord/types.ts diff --git a/README.md b/README.md index d39663a..d834927 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,11 @@ # Remote MCP 🚀 -**Create and connect MCP servers to your favorite AI clients - no complex setup required!** +**Easily create and connect MCP servers to your favorite AI clients — no complex setup required!** -[🌐 Try Remote MCP](https://remotemcp.tech) | [📖 What is MCP?](#what-is-mcp) | [🚀 Getting Started](#getting-started) +[🌐 Try Remote MCP](https://remotemcp.tech) | [📖 What is MCP?](#what-is-mcp) | [💡 Why Remote MCP?](#why-remote-mcp) | [📱 Available Apps](#available-apps) | [🚀 Getting Started](#getting-started) --- @@ -17,11 +17,7 @@ Remote MCP is a cloud-based platform that lets you easily create and manage **Model Context Protocol (MCP) servers** and connect them to your favorite AI clients like Claude Desktop, Cursor, or any MCP-compatible application. -Think of it as a bridge between your AI assistant and the apps you use every day - GitHub, Slack, YouTube, PostgreSQL, and many more! - - - -https://github.com/user-attachments/assets/3ddedf4e-571b-4e78-a46c-f15df8c4fe56 +Think of it as a bridge between your AI assistant and the apps you use every day — GitHub, Slack, YouTube, PostgreSQL, and many more! ## What is MCP? @@ -37,7 +33,7 @@ The **Model Context Protocol (MCP)** is an open standard that enables AI assista ### How MCP Works -``` +```text ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ AI Client │────│ MCP Server │────│ Your App │ │ (Claude) │ │ (Remote MCP)│ │ (GitHub) │ @@ -46,7 +42,7 @@ The **Model Context Protocol (MCP)** is an open standard that enables AI assista 1. **AI Client**: Your favorite AI assistant (Claude Desktop, Cursor, etc.) 2. **MCP Server**: Acts as a bridge (that's what Remote MCP provides!) -3. **Your App**: The service you want to connect (GitHub, Slack, etc.) +3. **Your App**: The service you want to connect to (GitHub, Slack, etc.) ## Why Remote MCP? @@ -56,7 +52,7 @@ No need to run local servers or manage complex configurations. Just create, conf ### 🔒 **Secure & Reliable** -Your credentials are encrypted and managed securely. We handle authentication, API limits, and security. +Your credentials are encrypted and managed securely. We handle authentication, API rate limits, and security best practices. ### 🌍 **Always Available** @@ -64,7 +60,7 @@ Cloud-hosted servers that work 24/7, accessible from any MCP client. ### 📊 **Visual Management** -Easy-to-use dashboard to manage your servers, connections, and monitor usage. +Easy-to-use dashboard to manage your servers, connections, and monitor usage in real-time. ## Available Apps @@ -78,6 +74,7 @@ Remote MCP supports integration with popular apps and services: ### Communication & Productivity - ** Slack** - Send messages, search conversations, and manage channels (5 tools) +- ** Discord** - Send messages, manage servers, channels, and reactions (6 tools) - ** Notion** - Database queries, page management, and content creation (18 tools) - ** Spotify** - Music playback control, playlist management, and discovery (19 tools) - ** Google Drive** - File management and Google Sheets operations (10 tools) @@ -92,18 +89,18 @@ Remote MCP supports integration with popular apps and services: ### Utilities - ** Fetch** - HTTP requests and web content fetching in multiple formats (4 tools) -> **Total: 159+ tools across 11 integrated applications** +> **Total: 165+ tools across 12 integrated applications** -_New apps and tools are being added regularly! Have a specific integration in mind? Let us know!_ +_New apps and tools are being added regularly! Have a specific integration in mind? [Let us know!](https://remotemcp.tech)_ ## Getting Started ### 1. Create Your MCP Server 1. Visit [remotemcp.tech](https://remotemcp.tech) -2. Sign up with Google or GitHub +2. Sign up with Google or GitHub (it's quick and free!) 3. Click "Add Server" to create your first MCP server -4. Choose the apps you want to connect (GitHub, Slack, etc.) +4. Choose the apps you want to connect (GitHub, Slack, Discord, etc.) ### 2. Configure App Connections @@ -117,7 +114,7 @@ _New apps and tools are being added regularly! Have a specific integration in mi Add your Remote MCP server to your AI client: **For VS Code & Cursor:** -Simply click the **"Add to VS Code"** or **"Add to Cursor"** button in your server dashboard - it automatically configures everything for you! +Simply click the **"Add to VS Code"** or **"Add to Cursor"** button in your server dashboard — it automatically configures everything for you! **For Claude Desktop:** Add to your `claude_desktop_config.json`: @@ -136,7 +133,7 @@ Add to your `claude_desktop_config.json`: } ``` -**For Other Clients:** +**For Other MCP Clients:** Use the MCP endpoint URL: `https://remotemcp.tech/api/mcp/YOUR_SERVER_ID` ### 4. Start Using! @@ -150,6 +147,7 @@ Your AI assistant can now interact with your connected apps. Try asking: **Communication & Content:** - "Send a message to the #general Slack channel" +- "Post an announcement in my Discord server" - "Create a new page in my Notion workspace" - "Search for recent videos about AI on YouTube" @@ -164,7 +162,7 @@ We welcome contributions! Check out our [contributing guidelines](CONTRIBUTING.m ## License -This project is licensed under the MIT License - see the [MIT](LICENSE) file for details. +This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details. --- diff --git a/src/app/mcp/apps/discord/common.ts b/src/app/mcp/apps/discord/common.ts new file mode 100644 index 0000000..e9dd51c --- /dev/null +++ b/src/app/mcp/apps/discord/common.ts @@ -0,0 +1,9 @@ +import { McpAppAuth } from "../../mcp-app/property"; + +export const discordAuth = McpAppAuth.OAuth2({ + description: "Connect to Discord to send messages and manage channels", + authUrl: "https://discord.com/api/oauth2/authorize", + tokenUrl: "https://discord.com/api/oauth2/token", + required: true, + scope: ["bot", "messages.read", "guilds", "guilds.members.read", "identify"], +}); diff --git a/src/app/mcp/apps/discord/discord-client.ts b/src/app/mcp/apps/discord/discord-client.ts new file mode 100644 index 0000000..61a7cc9 --- /dev/null +++ b/src/app/mcp/apps/discord/discord-client.ts @@ -0,0 +1,211 @@ +import type { OAuth2Props } from "@/app/mcp/mcp-app/property"; +import type { OAuth2Property } from "@/app/mcp/mcp-app/property/authentication/oauth2-prop"; +import type { McpRequestHandlerExtra } from "../../mcp-app/tools"; +import type { + DiscordChannel, + DiscordEmbed, + DiscordGuild, + DiscordMessage, + DiscordMessagePayload, + DiscordUser, +} from "./types"; + +const DISCORD_API_BASE = "https://discord.com/api/v10"; + +/** + * Discord API client using OAuth2 access token + */ +export class DiscordClient { + private accessToken: string; + + constructor(accessToken: string) { + this.accessToken = accessToken; + } + + private async makeRequest( + endpoint: string, + options: RequestInit = {}, + ): Promise { + const url = `${DISCORD_API_BASE}${endpoint}`; + const headers = { + Authorization: `Bearer ${this.accessToken}`, + "Content-Type": "application/json", + ...options.headers, + }; + + const response = await fetch(url, { + ...options, + headers, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Discord API Error (${response.status}): ${errorText}`); + } + + return response.json(); + } + + /** + * Get current user information + */ + async getCurrentUser(): Promise { + return this.makeRequest("/users/@me"); + } + + /** + * Get user's guilds (servers) + */ + async getUserGuilds(): Promise { + return this.makeRequest("/users/@me/guilds"); + } + + /** + * Get guild channels + */ + async getGuildChannels(guildId: string): Promise { + return this.makeRequest(`/guilds/${guildId}/channels`); + } + + /** + * Get channel information + */ + async getChannel(channelId: string): Promise { + return this.makeRequest(`/channels/${channelId}`); + } + + /** + * Send a message to a channel + */ + async sendMessage( + channelId: string, + content: string, + embeds?: DiscordEmbed[], + ): Promise { + const payload: DiscordMessagePayload = { content }; + if (embeds && embeds.length > 0) { + payload.embeds = embeds; + } + + return this.makeRequest(`/channels/${channelId}/messages`, { + method: "POST", + body: JSON.stringify(payload), + }); + } + + /** + * Get messages from a channel + */ + async getChannelMessages( + channelId: string, + options: { + limit?: number; + before?: string; + after?: string; + around?: string; + } = {}, + ): Promise { + const params = new URLSearchParams(); + if (options.limit) params.append("limit", options.limit.toString()); + if (options.before) params.append("before", options.before); + if (options.after) params.append("after", options.after); + if (options.around) params.append("around", options.around); + + const query = params.toString(); + const endpoint = `/channels/${channelId}/messages${query ? `?${query}` : ""}`; + + return this.makeRequest(endpoint); + } + + /** + * Add reaction to a message + */ + async addReaction( + channelId: string, + messageId: string, + emoji: string, + ): Promise { + return this.makeRequest( + `/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/@me`, + { method: "PUT" }, + ); + } + + /** + * Remove reaction from a message + */ + async removeReaction( + channelId: string, + messageId: string, + emoji: string, + ): Promise { + return this.makeRequest( + `/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/@me`, + { method: "DELETE" }, + ); + } + + /** + * Get guild members + */ + async getGuildMembers(guildId: string, limit = 100): Promise { + return this.makeRequest( + `/guilds/${guildId}/members?limit=${limit}`, + ); + } +} + +/** + * Creates a Discord client using the OAuth2 access token + */ +export function createDiscordClient( + extra: McpRequestHandlerExtra>, +): DiscordClient { + if (!extra.auth?.access_token) { + throw new Error("No access token available for Discord API"); + } + + return new DiscordClient(extra.auth.access_token); +} + +/** + * Format error messages from Discord API responses + */ +export function formatDiscordError(error: unknown): string { + if (error instanceof Error) { + return `Discord API Error: ${error.message}`; + } + + return `Discord API Error: ${String(error)}`; +} + +/** + * Parse channel ID to handle different formats + */ +export function parseChannelId(channelId: string): string { + // Remove # prefix if present + if (channelId.startsWith("#")) { + return channelId.slice(1); + } + return channelId; +} + +/** + * Format a Discord message for display + */ +export function formatDiscordMessage(message: DiscordMessage) { + return { + id: message.id, + content: message.content, + author: { + id: message.author.id, + username: message.author.username, + discriminator: message.author.discriminator, + }, + timestamp: message.timestamp, + channel_id: message.channel_id, + guild_id: message.guild_id, + embeds: message.embeds, + attachments: message.attachments, + }; +} diff --git a/src/app/mcp/apps/discord/index.ts b/src/app/mcp/apps/discord/index.ts new file mode 100644 index 0000000..6f5f078 --- /dev/null +++ b/src/app/mcp/apps/discord/index.ts @@ -0,0 +1,17 @@ +import { createMcpApp } from "../../mcp-app"; +import { McpAppCategory } from "../../mcp-app/app-metadata"; +import { discordAuth } from "./common"; +import { discordTools } from "./tools"; + +export const discordMcpApp = createMcpApp({ + name: "discord", + displayName: "Discord", + description: "Discord MCP App for accessing Discord servers and channels", + logo: { + type: "url", + url: "https://api.iconify.design/logos:discord-icon.svg", + }, + categories: [McpAppCategory.COMMUNICATION], + auth: discordAuth, + tools: discordTools, +}); diff --git a/src/app/mcp/apps/discord/tools/add-reaction.ts b/src/app/mcp/apps/discord/tools/add-reaction.ts new file mode 100644 index 0000000..fc82626 --- /dev/null +++ b/src/app/mcp/apps/discord/tools/add-reaction.ts @@ -0,0 +1,63 @@ +import { createParameterizedTool } from "@/app/mcp/mcp-app/tools"; +import { z } from "zod"; +import { discordAuth } from "../common"; +import { + createDiscordClient, + formatDiscordError, + parseChannelId, +} from "../discord-client"; + +export const addReactionTool = createParameterizedTool({ + name: "add_reaction", + auth: discordAuth, + description: + "Add a reaction (emoji) to a Discord message. Supports both Unicode emojis and custom server emojis.", + paramsSchema: { + channel_id: z + .string() + .describe( + "ID of the Discord channel containing the message. Can include # prefix (e.g., #general) or just the channel ID.", + ), + message_id: z + .string() + .describe("ID of the message to add the reaction to."), + emoji: z + .string() + .describe( + "Emoji to add as a reaction. Can be Unicode emoji (e.g., '👍', '❤️') or custom emoji name (e.g., 'custom_emoji_name').", + ), + }, + callback: async (params, extra) => { + try { + const client = createDiscordClient(extra); + const channelId = parseChannelId(params.channel_id); + + await client.addReaction(channelId, params.message_id, params.emoji); + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify({ + success: true, + channel_id: channelId, + message_id: params.message_id, + emoji: params.emoji, + action: "added", + }), + }, + ], + }; + } catch (error) { + console.error("Error adding Discord reaction:", error); + return { + content: [ + { + type: "text" as const, + text: formatDiscordError(error), + }, + ], + }; + } + }, +}); diff --git a/src/app/mcp/apps/discord/tools/get-messages.ts b/src/app/mcp/apps/discord/tools/get-messages.ts new file mode 100644 index 0000000..7d3de6f --- /dev/null +++ b/src/app/mcp/apps/discord/tools/get-messages.ts @@ -0,0 +1,89 @@ +import { createParameterizedTool } from "@/app/mcp/mcp-app/tools"; +import { z } from "zod"; +import { discordAuth } from "../common"; +import { + createDiscordClient, + formatDiscordError, + formatDiscordMessage, + parseChannelId, +} from "../discord-client"; + +export const getMessagesTool = createParameterizedTool({ + name: "get_messages", + auth: discordAuth, + description: + "Get messages from a Discord channel. Can retrieve recent messages or messages around a specific message ID.", + paramsSchema: { + channel_id: z + .string() + .describe( + "ID of the Discord channel to get messages from. Can include # prefix (e.g., #general) or just the channel ID.", + ), + limit: z + .number() + .min(1) + .max(100) + .default(50) + .describe("Number of messages to retrieve (1-100, default: 50)."), + before: z + .string() + .optional() + .describe( + "Get messages before this message ID (for pagination backwards).", + ), + after: z + .string() + .optional() + .describe( + "Get messages after this message ID (for pagination forwards).", + ), + around: z + .string() + .optional() + .describe("Get messages around this message ID (useful for context)."), + }, + callback: async (params, extra) => { + try { + const client = createDiscordClient(extra); + const channelId = parseChannelId(params.channel_id); + + const messages = await client.getChannelMessages(channelId, { + limit: params.limit, + before: params.before, + after: params.after, + around: params.around, + }); + + const formattedMessages = messages.map(formatDiscordMessage); + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify({ + channel_id: channelId, + messages: formattedMessages, + count: formattedMessages.length, + pagination: { + before: params.before, + after: params.after, + around: params.around, + limit: params.limit, + }, + }), + }, + ], + }; + } catch (error) { + console.error("Error getting Discord messages:", error); + return { + content: [ + { + type: "text" as const, + text: formatDiscordError(error), + }, + ], + }; + } + }, +}); diff --git a/src/app/mcp/apps/discord/tools/get-user-info.ts b/src/app/mcp/apps/discord/tools/get-user-info.ts new file mode 100644 index 0000000..8efb3c5 --- /dev/null +++ b/src/app/mcp/apps/discord/tools/get-user-info.ts @@ -0,0 +1,58 @@ +import { createParameterizedTool } from "@/app/mcp/mcp-app/tools"; +import { z } from "zod"; +import { discordAuth } from "../common"; +import { createDiscordClient, formatDiscordError } from "../discord-client"; + +export const getUserInfoTool = createParameterizedTool({ + name: "get_user_info", + auth: discordAuth, + description: + "Get information about the currently authenticated Discord user.", + paramsSchema: {}, + callback: async (params, extra) => { + try { + const client = createDiscordClient(extra); + const user = await client.getCurrentUser(); + + const userInfo = { + id: user.id, + username: user.username, + discriminator: user.discriminator, + global_name: user.global_name, + avatar: user.avatar, + bot: user.bot, + system: user.system, + mfa_enabled: user.mfa_enabled, + banner: user.banner, + accent_color: user.accent_color, + locale: user.locale, + verified: user.verified, + email: user.email, + flags: user.flags, + premium_type: user.premium_type, + public_flags: user.public_flags, + }; + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify({ + user: userInfo, + }), + }, + ], + }; + } catch (error) { + console.error("Error getting Discord user info:", error); + return { + content: [ + { + type: "text" as const, + text: formatDiscordError(error), + }, + ], + }; + } + }, +}); diff --git a/src/app/mcp/apps/discord/tools/index.ts b/src/app/mcp/apps/discord/tools/index.ts new file mode 100644 index 0000000..dc0f2cd --- /dev/null +++ b/src/app/mcp/apps/discord/tools/index.ts @@ -0,0 +1,15 @@ +import { addReactionTool } from "./add-reaction"; +import { getMessagesTool } from "./get-messages"; +import { getUserInfoTool } from "./get-user-info"; +import { listChannelsTool } from "./list-channels"; +import { listGuildsTool } from "./list-guilds"; +import { sendMessageTool } from "./send-message"; + +export const discordTools = [ + getUserInfoTool, + listGuildsTool, + listChannelsTool, + sendMessageTool, + getMessagesTool, + addReactionTool, +]; diff --git a/src/app/mcp/apps/discord/tools/list-channels.ts b/src/app/mcp/apps/discord/tools/list-channels.ts new file mode 100644 index 0000000..e7ba254 --- /dev/null +++ b/src/app/mcp/apps/discord/tools/list-channels.ts @@ -0,0 +1,70 @@ +import { createParameterizedTool } from "@/app/mcp/mcp-app/tools"; +import { z } from "zod"; +import { discordAuth } from "../common"; +import { createDiscordClient, formatDiscordError } from "../discord-client"; + +export const listChannelsTool = createParameterizedTool({ + name: "list_channels", + auth: discordAuth, + description: + "List all channels in a Discord server (guild). Shows text channels, voice channels, and categories.", + paramsSchema: { + guild_id: z + .string() + .describe("ID of the Discord server (guild) to list channels from."), + }, + callback: async (params, extra) => { + try { + const client = createDiscordClient(extra); + const channels = await client.getGuildChannels(params.guild_id); + + const formattedChannels = channels.map((channel) => ({ + id: channel.id, + name: channel.name, + type: channel.type, + position: channel.position, + parent_id: channel.parent_id, + topic: channel.topic, + nsfw: channel.nsfw, + permission_overwrites: channel.permission_overwrites, + })); + + // Group channels by type for better organization + const channelsByType = { + text: formattedChannels.filter((c) => c.type === 0), + voice: formattedChannels.filter((c) => c.type === 2), + category: formattedChannels.filter((c) => c.type === 4), + announcement: formattedChannels.filter((c) => c.type === 5), + thread: formattedChannels.filter((c) => [10, 11, 12].includes(c.type)), + forum: formattedChannels.filter((c) => c.type === 15), + other: formattedChannels.filter( + (c) => ![0, 2, 4, 5, 10, 11, 12, 15].includes(c.type), + ), + }; + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify({ + guild_id: params.guild_id, + channels: formattedChannels, + channels_by_type: channelsByType, + total_count: formattedChannels.length, + }), + }, + ], + }; + } catch (error) { + console.error("Error listing Discord channels:", error); + return { + content: [ + { + type: "text" as const, + text: formatDiscordError(error), + }, + ], + }; + } + }, +}); diff --git a/src/app/mcp/apps/discord/tools/list-guilds.ts b/src/app/mcp/apps/discord/tools/list-guilds.ts new file mode 100644 index 0000000..7928dd9 --- /dev/null +++ b/src/app/mcp/apps/discord/tools/list-guilds.ts @@ -0,0 +1,49 @@ +import { createParameterizedTool } from "@/app/mcp/mcp-app/tools"; +import { z } from "zod"; +import { discordAuth } from "../common"; +import { createDiscordClient, formatDiscordError } from "../discord-client"; + +export const listGuildsTool = createParameterizedTool({ + name: "list_guilds", + auth: discordAuth, + description: + "List all Discord servers (guilds) that the authenticated user has access to.", + paramsSchema: {}, + callback: async (params, extra) => { + try { + const client = createDiscordClient(extra); + const guilds = await client.getUserGuilds(); + + const formattedGuilds = guilds.map((guild) => ({ + id: guild.id, + name: guild.name, + icon: guild.icon, + owner: guild.owner, + permissions: guild.permissions, + features: guild.features, + })); + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify({ + guilds: formattedGuilds, + count: formattedGuilds.length, + }), + }, + ], + }; + } catch (error) { + console.error("Error listing Discord guilds:", error); + return { + content: [ + { + type: "text" as const, + text: formatDiscordError(error), + }, + ], + }; + } + }, +}); diff --git a/src/app/mcp/apps/discord/tools/send-message.ts b/src/app/mcp/apps/discord/tools/send-message.ts new file mode 100644 index 0000000..e8ec8ec --- /dev/null +++ b/src/app/mcp/apps/discord/tools/send-message.ts @@ -0,0 +1,110 @@ +import { createParameterizedTool } from "@/app/mcp/mcp-app/tools"; +import { z } from "zod"; +import { discordAuth } from "../common"; +import { + createDiscordClient, + formatDiscordError, + parseChannelId, +} from "../discord-client"; + +export const sendMessageTool = createParameterizedTool({ + name: "send_message", + auth: discordAuth, + description: + "Send a message to a Discord channel. Supports both text content and embeds.", + paramsSchema: { + channel_id: z + .string() + .describe( + "ID of the Discord channel to send the message to. Can include # prefix (e.g., #general) or just the channel ID.", + ), + content: z + .string() + .describe( + "The message content to send. Supports Discord markdown formatting.", + ), + embeds: z + .array( + z.object({ + title: z.string().optional(), + description: z.string().optional(), + color: z.number().optional(), + url: z.string().optional(), + timestamp: z.string().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(), + }), + ) + .optional() + .describe("Optional array of embed objects to include with the message."), + }, + callback: async (params, extra) => { + try { + const client = createDiscordClient(extra); + const channelId = parseChannelId(params.channel_id); + + const result = await client.sendMessage( + channelId, + params.content, + params.embeds, + ); + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify({ + success: true, + message: { + id: result.id, + channel_id: result.channel_id, + content: result.content, + timestamp: result.timestamp, + }, + }), + }, + ], + }; + } catch (error) { + console.error("Error sending Discord message:", error); + return { + content: [ + { + type: "text" as const, + text: formatDiscordError(error), + }, + ], + }; + } + }, +}); diff --git a/src/app/mcp/apps/discord/types.ts b/src/app/mcp/apps/discord/types.ts new file mode 100644 index 0000000..8ac8acc --- /dev/null +++ b/src/app/mcp/apps/discord/types.ts @@ -0,0 +1,431 @@ +// Discord API types based on Discord API documentation + +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?: DiscordOverwrite[]; + 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; + flags?: number; + total_message_sent?: number; +} + +export interface DiscordMessage { + id: string; + channel_id: string; + guild_id?: string; + author: DiscordUser; + member?: DiscordGuildMember; + 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[]; + stickers?: DiscordSticker[]; +} + +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 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 DiscordEmoji { + id?: string; + name?: string; + roles?: string[]; + user?: DiscordUser; + require_colons?: boolean; + managed?: boolean; + animated?: boolean; + available?: boolean; +} + +export interface DiscordOverwrite { + 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 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 DiscordChannelMention { + id: string; + guild_id: string; + type: number; + name: string; +} + +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 DiscordMessageActivity { + type: number; + party_id?: string; +} + +export interface DiscordApplication { + id: string; + name: string; + icon?: string; + description: string; + rpc_origins?: string[]; + bot_public: boolean; + bot_require_code_grant: boolean; + terms_of_service_url?: string; + privacy_policy_url?: string; + owner?: DiscordUser; + summary: string; + verify_key: string; + team?: DiscordTeam; + 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 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 DiscordComponent { + type: number; + custom_id?: string; + disabled?: boolean; + style?: number; + label?: string; + emoji?: DiscordEmoji; + url?: string; + options?: DiscordSelectOption[]; + placeholder?: string; + min_values?: number; + max_values?: number; + components?: DiscordComponent[]; +} + +export interface DiscordSelectOption { + label: string; + value: string; + description?: string; + emoji?: DiscordEmoji; + default?: boolean; +} + +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 DiscordTeam { + icon?: string; + 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; +} + +// API Response types +export interface DiscordAPIResponse { + data?: T; + error?: { + code: number; + message: string; + }; +} + +// Message payload for sending messages +export interface DiscordMessagePayload { + content?: string; + embeds?: DiscordEmbed[]; + allowed_mentions?: { + parse?: string[]; + roles?: string[]; + users?: string[]; + replied_user?: boolean; + }; + message_reference?: DiscordMessageReference; + components?: DiscordComponent[]; + sticker_ids?: string[]; + files?: File[]; + flags?: number; +} diff --git a/src/app/mcp/apps/index.ts b/src/app/mcp/apps/index.ts index 8812abd..91d4a62 100644 --- a/src/app/mcp/apps/index.ts +++ b/src/app/mcp/apps/index.ts @@ -1,6 +1,7 @@ import type { McpApp } from "../mcp-app"; import { atlassianMcpApp } from "./atlassian"; import { braveMcpApp } from "./brave"; +import { discordMcpApp } from "./discord"; import { fetchMcpApp } from "./fetch"; import { firecrawlMcpApp } from "./firecrawl"; import { githubMcpApp } from "./github"; @@ -21,6 +22,7 @@ export const mcpApps = [ firecrawlMcpApp, youtubeMcpApp, slackMcpApp, + discordMcpApp, braveMcpApp, postgresMcpApp, notionMcpApp,