Initial commit
This commit is contained in:
commit
bb79516c97
|
@ -0,0 +1,2 @@
|
||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Node.js
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log
|
||||||
|
yarn-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# Environment Variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Build Output
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# IDE and Editors
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Operating Systems
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 BarryY
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,152 @@
|
||||||
|
# MCP-Discord
|
||||||
|
|
||||||
|
A Discord MCP (Model Context Protocol) server that enables AI assistants to interact with the Discord platform.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
MCP-Discord provides the following Discord-related functionalities:
|
||||||
|
|
||||||
|
- Login to Discord bot
|
||||||
|
- Send messages to specified channels
|
||||||
|
- Get server information
|
||||||
|
- Retrieve forum channel lists
|
||||||
|
- Create/delete/reply to forum posts
|
||||||
|
- Read channel messages
|
||||||
|
- Create/delete text channels
|
||||||
|
- Add/remove message reactions
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Prerequisites](#prerequisites)
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Configuration](#configuration)
|
||||||
|
- [Tools Documentation](#tools-documentation)
|
||||||
|
- [Basic Functions](#basic-functions)
|
||||||
|
- [Channel Management](#channel-management)
|
||||||
|
- [Forum Functions](#forum-functions)
|
||||||
|
- [Messages and Reactions](#messages-and-reactions)
|
||||||
|
- [Development](#development)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Node.js (v16.0.0 or higher)
|
||||||
|
- npm (v7.0.0 or higher)
|
||||||
|
- A Discord bot with appropriate permissions
|
||||||
|
- Bot token (obtainable from the [Discord Developer Portal](https://discord.com/developers/applications))
|
||||||
|
- Message Content Intent enabled
|
||||||
|
- Server Members Intent enabled
|
||||||
|
- Presence Intent enabled
|
||||||
|
- Permissions in your Discord server:
|
||||||
|
- Send Messages
|
||||||
|
- Create Public Threads
|
||||||
|
- Send Messages in Threads
|
||||||
|
- Manage Threads
|
||||||
|
- Manage Channels
|
||||||
|
- Add Reactions
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://github.com/barryyip0625/MCP-Discord.git
|
||||||
|
cd MCP-Discord
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Compile TypeScript
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
A Discord bot token is required for proper operation. You can provide it in two ways:
|
||||||
|
|
||||||
|
1. Environment variables:
|
||||||
|
```
|
||||||
|
DISCORD_TOKEN=your_discord_bot_token
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Using the `--config` parameter when launching:
|
||||||
|
```
|
||||||
|
node path/to/MCP-Discord/build/index.js --config "{\"DISCORD_TOKEN\":\"your_discord_bot_token\"}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage with Claude/Cursor
|
||||||
|
- Claude
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"discord": {
|
||||||
|
"command": "node",
|
||||||
|
"args": [
|
||||||
|
"path/to/MCP-Discord/build/index.js"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"DISCORD_TOKEN": "your_discord_bot_token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Cursor
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"discord": {
|
||||||
|
"command": "cmd",
|
||||||
|
"args": [
|
||||||
|
"/c",
|
||||||
|
"node",
|
||||||
|
"path/to/MCP-Discord/build/index.js"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"DISCORD_TOKEN": "your_discord_bot_token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tools Documentation
|
||||||
|
|
||||||
|
### Basic Functions
|
||||||
|
|
||||||
|
- `discord_login`: Login to Discord
|
||||||
|
- `discord_send`: Send a message to a specified channel
|
||||||
|
- `discord_get_server_info`: Get Discord server information
|
||||||
|
|
||||||
|
### Channel Management
|
||||||
|
|
||||||
|
- `discord_create_text_channel`: Create a text channel
|
||||||
|
- `discord_delete_channel`: Delete a channel
|
||||||
|
|
||||||
|
### Forum Functions
|
||||||
|
|
||||||
|
- `discord_get_forum_channels`: Get a list of forum channels
|
||||||
|
- `discord_create_forum_post`: Create a forum post
|
||||||
|
- `discord_get_forum_post`: Get a forum post
|
||||||
|
- `discord_reply_to_forum`: Reply to a forum post
|
||||||
|
- `discord_delete_forum_post`: Delete a forum post
|
||||||
|
|
||||||
|
### Messages and Reactions
|
||||||
|
|
||||||
|
- `discord_read_messages`: Read channel messages
|
||||||
|
- `discord_add_reaction`: Add a reaction to a message
|
||||||
|
- `discord_add_multiple_reactions`: Add multiple reactions to a message
|
||||||
|
- `discord_remove_reaction`: Remove a reaction from a message
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development mode
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "mcp-discord",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"dev": "ts-node src/index.ts"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"description": "",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.17.26",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.8.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.7.0",
|
||||||
|
"discord.js": "^14.18.0",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
|
"zod": "^3.24.2"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,845 @@
|
||||||
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Client, GatewayIntentBits, Events, TextChannel, ForumChannel, ChannelType } from "discord.js";
|
||||||
|
|
||||||
|
// Configuration parsing
|
||||||
|
let config: any = {};
|
||||||
|
|
||||||
|
// Read configuration from environment variables
|
||||||
|
if (process.env.DISCORD_TOKEN) {
|
||||||
|
config.DISCORD_TOKEN = process.env.DISCORD_TOKEN;
|
||||||
|
console.log("Config loaded from environment variables. Discord token available:", !!config.DISCORD_TOKEN);
|
||||||
|
if (config.DISCORD_TOKEN) {
|
||||||
|
console.log("Token length:", config.DISCORD_TOKEN.length);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Try to parse configuration from command line arguments (for backward compatibility)
|
||||||
|
const configArgIndex = process.argv.indexOf('--config');
|
||||||
|
if (configArgIndex !== -1 && configArgIndex < process.argv.length - 1) {
|
||||||
|
try {
|
||||||
|
let configStr = process.argv[configArgIndex + 1];
|
||||||
|
|
||||||
|
// Print raw configuration string for debugging
|
||||||
|
console.log("Raw config string:", configStr);
|
||||||
|
|
||||||
|
// Try to parse JSON
|
||||||
|
config = JSON.parse(configStr);
|
||||||
|
console.log("Config parsed successfully. Discord token available:", !!config.DISCORD_TOKEN);
|
||||||
|
|
||||||
|
if (config.DISCORD_TOKEN) {
|
||||||
|
console.log("Token length:", config.DISCORD_TOKEN.length);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to parse config argument:", error);
|
||||||
|
console.error("Raw config argument:", process.argv[configArgIndex + 1]);
|
||||||
|
|
||||||
|
// Try to read arguments directly (for debugging)
|
||||||
|
console.log("All arguments:", process.argv);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("No config found in environment variables or command line arguments");
|
||||||
|
console.log("All arguments:", process.argv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Discord client
|
||||||
|
const client = new Client({
|
||||||
|
intents: [
|
||||||
|
GatewayIntentBits.Guilds,
|
||||||
|
GatewayIntentBits.GuildMessages,
|
||||||
|
GatewayIntentBits.MessageContent
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create an MCP server
|
||||||
|
const server = new McpServer({
|
||||||
|
name: "MCP-Discord",
|
||||||
|
version: "1.0.0"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a test tool
|
||||||
|
server.tool(
|
||||||
|
"test",
|
||||||
|
{ name: z.string() },
|
||||||
|
async () => ({
|
||||||
|
content: [{ type: "text", text: `test success` }]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Discord login tool
|
||||||
|
server.tool(
|
||||||
|
"discord_login",
|
||||||
|
{ random_string: z.string().optional() },
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const token = config.DISCORD_TOKEN;
|
||||||
|
if (!token) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: "Discord token not found in config. Make sure the --config parameter is correctly set." }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.login(token);
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Successfully logged in to Discord : ${client.user?.tag}` }]
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Login failed: ${error}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Discord send message tool
|
||||||
|
server.tool(
|
||||||
|
"discord_send",
|
||||||
|
{
|
||||||
|
channelId: z.string(),
|
||||||
|
message: z.string()
|
||||||
|
},
|
||||||
|
async ({ channelId, message }) => {
|
||||||
|
try {
|
||||||
|
if (!client.isReady()) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = await client.channels.fetch(channelId);
|
||||||
|
if (!channel || !channel.isTextBased()) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Cannot find text channel ID: ${channelId}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure channel is text-based and can send messages
|
||||||
|
if ('send' in channel) {
|
||||||
|
await channel.send(message);
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Message successfully sent to channel ID: ${channelId}` }]
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `This channel type does not support sending messages` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Send message failed: ${error}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get forum channels tool
|
||||||
|
server.tool(
|
||||||
|
"discord_get_forum_channels",
|
||||||
|
{
|
||||||
|
guildId: z.string()
|
||||||
|
},
|
||||||
|
async ({ guildId }) => {
|
||||||
|
try {
|
||||||
|
if (!client.isReady()) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const guild = await client.guilds.fetch(guildId);
|
||||||
|
if (!guild) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Cannot find guild with ID: ${guildId}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all channels from the guild
|
||||||
|
const channels = await guild.channels.fetch();
|
||||||
|
|
||||||
|
// Filter to get only forum channels
|
||||||
|
const forumChannels = channels.filter(channel => channel?.type === ChannelType.GuildForum);
|
||||||
|
|
||||||
|
if (forumChannels.size === 0) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `No forum channels found in guild: ${guild.name}` }]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format forum channels information
|
||||||
|
const forumInfo = forumChannels.map(channel => ({
|
||||||
|
id: channel.id,
|
||||||
|
name: channel.name,
|
||||||
|
topic: channel.topic || "No topic set"
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: JSON.stringify(forumInfo, null, 2) }]
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Failed to fetch forum channels: ${error}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create forum post tool
|
||||||
|
server.tool(
|
||||||
|
"discord_create_forum_post",
|
||||||
|
{
|
||||||
|
forumChannelId: z.string(),
|
||||||
|
title: z.string(),
|
||||||
|
content: z.string(),
|
||||||
|
tags: z.array(z.string()).optional()
|
||||||
|
},
|
||||||
|
async ({ forumChannelId, title, content, tags }) => {
|
||||||
|
try {
|
||||||
|
if (!client.isReady()) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = await client.channels.fetch(forumChannelId);
|
||||||
|
if (!channel || channel.type !== ChannelType.GuildForum) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Channel ID ${forumChannelId} is not a forum channel.` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const forumChannel = channel as ForumChannel;
|
||||||
|
|
||||||
|
// Get available tags in the forum
|
||||||
|
const availableTags = forumChannel.availableTags;
|
||||||
|
let selectedTagIds: string[] = [];
|
||||||
|
|
||||||
|
// If tags are provided, find their IDs
|
||||||
|
if (tags && tags.length > 0) {
|
||||||
|
selectedTagIds = availableTags
|
||||||
|
.filter(tag => tags.includes(tag.name))
|
||||||
|
.map(tag => tag.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the forum post
|
||||||
|
const thread = await forumChannel.threads.create({
|
||||||
|
name: title,
|
||||||
|
message: {
|
||||||
|
content: content
|
||||||
|
},
|
||||||
|
appliedTags: selectedTagIds.length > 0 ? selectedTagIds : undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Successfully created forum post "${title}" with ID: ${thread.id}`
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Failed to create forum post: ${error}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get forum post (thread) details tool
|
||||||
|
server.tool(
|
||||||
|
"discord_get_forum_post",
|
||||||
|
{
|
||||||
|
threadId: z.string()
|
||||||
|
},
|
||||||
|
async ({ threadId }) => {
|
||||||
|
try {
|
||||||
|
if (!client.isReady()) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const thread = await client.channels.fetch(threadId);
|
||||||
|
if (!thread || !(thread.isThread())) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Cannot find thread with ID: ${threadId}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get messages from the thread
|
||||||
|
const messages = await thread.messages.fetch({ limit: 10 });
|
||||||
|
|
||||||
|
const threadDetails = {
|
||||||
|
id: thread.id,
|
||||||
|
name: thread.name,
|
||||||
|
parentId: thread.parentId,
|
||||||
|
messageCount: messages.size,
|
||||||
|
createdAt: thread.createdAt,
|
||||||
|
messages: messages.map(msg => ({
|
||||||
|
id: msg.id,
|
||||||
|
content: msg.content,
|
||||||
|
author: msg.author.tag,
|
||||||
|
createdAt: msg.createdAt
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: JSON.stringify(threadDetails, null, 2) }]
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Failed to fetch forum post: ${error}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reply to forum post tool
|
||||||
|
server.tool(
|
||||||
|
"discord_reply_to_forum",
|
||||||
|
{
|
||||||
|
threadId: z.string(),
|
||||||
|
message: z.string()
|
||||||
|
},
|
||||||
|
async ({ threadId, message }) => {
|
||||||
|
try {
|
||||||
|
if (!client.isReady()) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const thread = await client.channels.fetch(threadId);
|
||||||
|
if (!thread || !(thread.isThread())) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Cannot find thread with ID: ${threadId}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('send' in thread)) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `This thread does not support sending messages` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the reply
|
||||||
|
const sentMessage = await thread.send(message);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Successfully replied to forum post. Message ID: ${sentMessage.id}`
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Failed to reply to forum post: ${error}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create text channel tool
|
||||||
|
server.tool(
|
||||||
|
"discord_create_text_channel",
|
||||||
|
{
|
||||||
|
guildId: z.string(),
|
||||||
|
channelName: z.string(),
|
||||||
|
topic: z.string().optional()
|
||||||
|
},
|
||||||
|
async ({ guildId, channelName, topic }) => {
|
||||||
|
try {
|
||||||
|
if (!client.isReady()) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const guild = await client.guilds.fetch(guildId);
|
||||||
|
if (!guild) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Cannot find guild with ID: ${guildId}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the text channel
|
||||||
|
const channel = await guild.channels.create({
|
||||||
|
name: channelName,
|
||||||
|
type: ChannelType.GuildText,
|
||||||
|
topic: topic
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Successfully created text channel "${channelName}" with ID: ${channel.id}`
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Failed to create text channel: ${error}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete channel tool
|
||||||
|
server.tool(
|
||||||
|
"discord_delete_channel",
|
||||||
|
{
|
||||||
|
channelId: z.string(),
|
||||||
|
reason: z.string().optional()
|
||||||
|
},
|
||||||
|
async ({ channelId, reason }) => {
|
||||||
|
try {
|
||||||
|
if (!client.isReady()) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = await client.channels.fetch(channelId);
|
||||||
|
if (!channel) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Cannot find channel with ID: ${channelId}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if channel can be deleted (has delete method)
|
||||||
|
if (!('delete' in channel)) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `This channel type does not support deletion or the bot lacks permissions` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the channel
|
||||||
|
await channel.delete(reason || "Channel deleted via API");
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Successfully deleted channel with ID: ${channelId}`
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Failed to delete channel: ${error}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Read messages from channel tool
|
||||||
|
server.tool(
|
||||||
|
"discord_read_messages",
|
||||||
|
{
|
||||||
|
channelId: z.string(),
|
||||||
|
limit: z.number().min(1).max(100).optional().default(50)
|
||||||
|
},
|
||||||
|
async ({ channelId, limit }) => {
|
||||||
|
try {
|
||||||
|
if (!client.isReady()) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = await client.channels.fetch(channelId);
|
||||||
|
if (!channel) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Cannot find channel with ID: ${channelId}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if channel has messages (text channel, thread, etc.)
|
||||||
|
if (!channel.isTextBased() || !('messages' in channel)) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Channel type does not support reading messages` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch messages
|
||||||
|
const messages = await channel.messages.fetch({ limit });
|
||||||
|
|
||||||
|
if (messages.size === 0) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `No messages found in channel` }]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format messages
|
||||||
|
const formattedMessages = messages.map(msg => ({
|
||||||
|
id: msg.id,
|
||||||
|
content: msg.content,
|
||||||
|
author: {
|
||||||
|
id: msg.author.id,
|
||||||
|
username: msg.author.username,
|
||||||
|
bot: msg.author.bot
|
||||||
|
},
|
||||||
|
timestamp: msg.createdAt,
|
||||||
|
attachments: msg.attachments.size,
|
||||||
|
embeds: msg.embeds.length,
|
||||||
|
replyTo: msg.reference ? msg.reference.messageId : null
|
||||||
|
})).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify({
|
||||||
|
channelId,
|
||||||
|
messageCount: formattedMessages.length,
|
||||||
|
messages: formattedMessages
|
||||||
|
}, null, 2)
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Failed to read messages: ${error}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get server information tool
|
||||||
|
server.tool(
|
||||||
|
"discord_get_server_info",
|
||||||
|
{
|
||||||
|
guildId: z.string()
|
||||||
|
},
|
||||||
|
async ({ guildId }) => {
|
||||||
|
try {
|
||||||
|
if (!client.isReady()) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const guild = await client.guilds.fetch(guildId);
|
||||||
|
if (!guild) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Cannot find guild with ID: ${guildId}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch additional guild data
|
||||||
|
await guild.fetch();
|
||||||
|
|
||||||
|
// Fetch channel information
|
||||||
|
const channels = await guild.channels.fetch();
|
||||||
|
|
||||||
|
// Categorize channels by type
|
||||||
|
const channelsByType = {
|
||||||
|
text: channels.filter(c => c?.type === ChannelType.GuildText).size,
|
||||||
|
voice: channels.filter(c => c?.type === ChannelType.GuildVoice).size,
|
||||||
|
category: channels.filter(c => c?.type === ChannelType.GuildCategory).size,
|
||||||
|
forum: channels.filter(c => c?.type === ChannelType.GuildForum).size,
|
||||||
|
announcement: channels.filter(c => c?.type === ChannelType.GuildAnnouncement).size,
|
||||||
|
stage: channels.filter(c => c?.type === ChannelType.GuildStageVoice).size,
|
||||||
|
total: channels.size
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch member count
|
||||||
|
const approximateMemberCount = guild.approximateMemberCount || "unknown";
|
||||||
|
|
||||||
|
// Format guild information
|
||||||
|
const guildInfo = {
|
||||||
|
id: guild.id,
|
||||||
|
name: guild.name,
|
||||||
|
description: guild.description,
|
||||||
|
icon: guild.iconURL(),
|
||||||
|
owner: guild.ownerId,
|
||||||
|
createdAt: guild.createdAt,
|
||||||
|
memberCount: approximateMemberCount,
|
||||||
|
channels: channelsByType,
|
||||||
|
features: guild.features,
|
||||||
|
premium: {
|
||||||
|
tier: guild.premiumTier,
|
||||||
|
subscriptions: guild.premiumSubscriptionCount
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: JSON.stringify(guildInfo, null, 2) }]
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Failed to fetch server info: ${error}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add reaction to message tool
|
||||||
|
server.tool(
|
||||||
|
"discord_add_reaction",
|
||||||
|
{
|
||||||
|
channelId: z.string(),
|
||||||
|
messageId: z.string(),
|
||||||
|
emoji: z.string()
|
||||||
|
},
|
||||||
|
async ({ channelId, messageId, emoji }) => {
|
||||||
|
try {
|
||||||
|
if (!client.isReady()) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = await client.channels.fetch(channelId);
|
||||||
|
if (!channel || !channel.isTextBased() || !('messages' in channel)) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Cannot find text channel with ID: ${channelId}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = await channel.messages.fetch(messageId);
|
||||||
|
if (!message) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Cannot find message with ID: ${messageId}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the reaction
|
||||||
|
await message.react(emoji);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Successfully added reaction ${emoji} to message ID: ${messageId}`
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Failed to add reaction: ${error}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add multiple reactions to message tool
|
||||||
|
server.tool(
|
||||||
|
"discord_add_multiple_reactions",
|
||||||
|
{
|
||||||
|
channelId: z.string(),
|
||||||
|
messageId: z.string(),
|
||||||
|
emojis: z.array(z.string())
|
||||||
|
},
|
||||||
|
async ({ channelId, messageId, emojis }) => {
|
||||||
|
try {
|
||||||
|
if (!client.isReady()) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = await client.channels.fetch(channelId);
|
||||||
|
if (!channel || !channel.isTextBased() || !('messages' in channel)) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Cannot find text channel with ID: ${channelId}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = await channel.messages.fetch(messageId);
|
||||||
|
if (!message) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Cannot find message with ID: ${messageId}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add each reaction sequentially
|
||||||
|
for (const emoji of emojis) {
|
||||||
|
await message.react(emoji);
|
||||||
|
// Small delay to prevent rate limiting
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Successfully added ${emojis.length} reactions to message ID: ${messageId}`
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Failed to add reactions: ${error}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove reaction from message tool
|
||||||
|
server.tool(
|
||||||
|
"discord_remove_reaction",
|
||||||
|
{
|
||||||
|
channelId: z.string(),
|
||||||
|
messageId: z.string(),
|
||||||
|
emoji: z.string(),
|
||||||
|
userId: z.string().optional()
|
||||||
|
},
|
||||||
|
async ({ channelId, messageId, emoji, userId }) => {
|
||||||
|
try {
|
||||||
|
if (!client.isReady()) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = await client.channels.fetch(channelId);
|
||||||
|
if (!channel || !channel.isTextBased() || !('messages' in channel)) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Cannot find text channel with ID: ${channelId}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = await channel.messages.fetch(messageId);
|
||||||
|
if (!message) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Cannot find message with ID: ${messageId}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the reactions
|
||||||
|
const reactions = message.reactions.cache;
|
||||||
|
|
||||||
|
// Find the specific reaction
|
||||||
|
const reaction = reactions.find(r => r.emoji.toString() === emoji || r.emoji.name === emoji);
|
||||||
|
|
||||||
|
if (!reaction) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Reaction ${emoji} not found on message ID: ${messageId}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
// Remove a specific user's reaction
|
||||||
|
await reaction.users.remove(userId);
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Successfully removed reaction ${emoji} from user ID: ${userId} on message ID: ${messageId}`
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Remove bot's reaction
|
||||||
|
await reaction.users.remove(client.user.id);
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Successfully removed bot's reaction ${emoji} from message ID: ${messageId}`
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Failed to remove reaction: ${error}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete forum post tool
|
||||||
|
server.tool(
|
||||||
|
"discord_delete_forum_post",
|
||||||
|
{
|
||||||
|
threadId: z.string(),
|
||||||
|
reason: z.string().optional()
|
||||||
|
},
|
||||||
|
async ({ threadId, reason }) => {
|
||||||
|
try {
|
||||||
|
if (!client.isReady()) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const thread = await client.channels.fetch(threadId);
|
||||||
|
if (!thread || !thread.isThread()) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Cannot find forum post/thread with ID: ${threadId}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the forum post/thread
|
||||||
|
await thread.delete(reason || "Forum post deleted via API");
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Successfully deleted forum post/thread with ID: ${threadId}`
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Failed to delete forum post: ${error}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Auto-login on startup if token is available
|
||||||
|
const autoLogin = async () => {
|
||||||
|
const token = config.DISCORD_TOKEN;
|
||||||
|
if (token) {
|
||||||
|
try {
|
||||||
|
await client.login(token);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Auto-login failed:", error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("No Discord token found in config, skipping auto-login");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start auto-login process
|
||||||
|
autoLogin();
|
||||||
|
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"outDir": "./build",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
Loading…
Reference in New Issue