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,