From acc6f5ce96290de588adc1567f2b15d7430b8f00 Mon Sep 17 00:00:00 2001 From: Kirill Sirotkin Date: Tue, 4 Nov 2025 12:48:59 +0200 Subject: [PATCH 1/4] fix: enable --transport flag override, set stdio as default for docker container --- Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 935fc06..32fd769 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,4 +20,6 @@ RUN npm run build EXPOSE 8080 # Default command to run the MCP server with HTTP transport -CMD ["node", "build/index.js", "--transport", "http", "--port", "8080"] \ No newline at end of file +# Use ENTRYPOINT + CMD for flexibility +ENTRYPOINT ["node", "build/index.js"] +CMD ["--transport", "stdio", "--port", "8080"] \ No newline at end of file From c5db23f33064e8effcb4584dd9704a84a75e36d9 Mon Sep 17 00:00:00 2001 From: Kirill Sirotkin Date: Tue, 4 Nov 2025 13:00:09 +0200 Subject: [PATCH 2/4] readme: update Installing via Docker section --- README.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a8407bd..f25072f 100644 --- a/README.md +++ b/README.md @@ -95,15 +95,30 @@ You can run mcp-discord using Docker. The Docker images are automatically built **Docker Hub Repository**: [barryy625/mcp-discord](https://hub.docker.com/r/barryy625/mcp-discord) +Docker container uses `stdio` by default. + ```bash # Pull the latest image docker pull barryy625/mcp-discord:latest # Run with environment variable -docker run -e DISCORD_TOKEN=your_discord_bot_token -p 8080:8080 barryy625/mcp-discord:latest +docker run -e DISCORD_TOKEN=your_discord_bot_token barryy625/mcp-discord:latest # Or run with command line config -docker run -p 8080:8080 barryy625/mcp-discord:latest --config "your_discord_bot_token" +docker run barryy625/mcp-discord:latest --config "your_discord_bot_token" +``` + +Alternatively, override the `--transport` and `--port` flags to run `http` on a port of your choosing. In this case, also map the ports. + +```bash +# Pull the latest image +docker pull barryy625/mcp-discord:latest + +# Run with environment variable +docker run -e DISCORD_TOKEN=your_discord_bot_token -p 8080:8080 barryy625/mcp-discord:latest --transport http --port 8081 + +# Or run with command line config +docker run -p 8080:8080 barryy625/mcp-discord:latest --config "your_discord_bot_token" --transport http --port 8081 ``` **Available Tags:** From 4c2621717ed07c791b0e6c1b6aa7a63358d9e33a Mon Sep 17 00:00:00 2001 From: robertlestak Date: Fri, 28 Nov 2025 10:51:44 -0800 Subject: [PATCH 3/4] Fix streamable HTTP transport to use SDK's handleRequest method - Remove custom /mcp endpoint handling that was causing 405 errors - Use StreamableHTTPServerTransport.handleRequest() to properly handle MCP protocol - Fixes compatibility with MCP Inspector and Kiro clients --- src/transport.ts | 574 ++--------------------------------------------- 1 file changed, 14 insertions(+), 560 deletions(-) diff --git a/src/transport.ts b/src/transport.ts index 1be5716..5f090af 100644 --- a/src/transport.ts +++ b/src/transport.ts @@ -65,571 +65,16 @@ export class StreamableHttpTransport implements MCPTransport { constructor(private port: number = 8080) { this.app = express(); this.app.use(express.json()); - this.setupEndpoints(); this.sessionId = `session_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`; info(`Created HTTP transport with session ID: ${this.sessionId}`); } - private setupEndpoints() { - // Handler for POST requests - this.app.post('/mcp', (req: Request, res: Response) => { - info('Received MCP request: ' + JSON.stringify(req.body)); - this.handleMcpRequest(req, res).catch(error => { - error('Unhandled error in MCP request: ' + String(error)); - }); - }); - - // Handler for non-POST methods - this.app.all('/mcp', (req: Request, res: Response) => { - if (req.method !== 'POST') { - res.status(405).json({ - jsonrpc: '2.0', - error: { - code: -32000, - message: 'Method not allowed. Use POST.', - }, - id: null, - }); - } - }); - } - - private async handleMcpRequest(req: Request, res: Response) { - try { - if (!this.server) { - return res.json({ - jsonrpc: '2.0', - error: { - code: -32603, - message: 'Server not initialized', - }, - id: req.body?.id || null, - }); - } - - info(`Request body (session ${this.sessionId}): ${JSON.stringify(req.body)}`); - - // Handle all tool requests in a generic way - if (!req.body.method) { - return res.status(400).json({ - jsonrpc: '2.0', - error: { - code: -32600, - message: 'Invalid Request: No method specified', - }, - id: req.body?.id || null, - }); - } - - // Handle all tools directly with proper error handling - try { - const method = req.body.method; - const params = req.body.params || {}; - - // Make sure toolContext is available for tool methods - if (!this.toolContext && method !== 'list_tools' && method !== 'initialize') { - return res.status(400).json({ - jsonrpc: '2.0', - error: { - code: -32603, - message: 'Tool context not initialized. Service may need to be restarted.', - }, - id: req.body?.id || null, - }); - } - - let result; - - // Handle each tool method directly - switch (method) { - case 'initialize': - // Handle initialize method for MCP protocol compliance - result = { - protocolVersion: "2025-03-26", - capabilities: { - tools: { - listChanged: false - }, - logging: {} - }, - serverInfo: { - name: "MCP-Discord", - version: "1.2.0" - } - }; - break; - - case 'notifications/initialized': - // Client indicates it's ready to begin normal operation - info("Client initialized. Starting normal operations."); - // No result needed for notifications - return res.json({ - jsonrpc: "2.0", - result: null, - id: req.body.id - }); - - case 'tools/list': - // New MCP method name format - result = { tools: toolList }; - break; - - case 'list_tools': - // Legacy method name for backward compatibility - result = { tools: toolList }; - break; - - case 'discord_login': - result = await loginHandler(params, this.toolContext!); - // Log client state after login - info(`Client state after login: ${JSON.stringify({ - isReady: this.toolContext!.client.isReady(), - hasToken: !!this.toolContext!.client.token, - user: this.toolContext!.client.user ? { - id: this.toolContext!.client.user.id, - tag: this.toolContext!.client.user.tag, - } : null - })}`); - break; - - // Make sure Discord client is logged in for other Discord API tools - // but return a proper JSON-RPC error rather than throwing an exception - case 'discord_send': - case 'discord_get_forum_channels': - case 'discord_create_forum_post': - case 'discord_get_forum_post': - case 'discord_reply_to_forum': - case 'discord_delete_forum_post': - case 'discord_create_text_channel': - case 'discord_delete_channel': - case 'discord_read_messages': - case 'discord_get_server_info': - case 'discord_add_reaction': - case 'discord_add_multiple_reactions': - case 'discord_remove_reaction': - case 'discord_delete_message': - case 'discord_create_webhook': - case 'discord_send_webhook_message': - case 'discord_edit_webhook': - case 'discord_delete_webhook': - case 'discord_create_category': - case 'discord_edit_category': - case 'discord_delete_category': - case 'discord_list_servers': - case 'discord_search_messages': - // Check if client is logged in - if (!this.toolContext!.client.isReady()) { - error(`Client not ready for method ${method}, client state: ${JSON.stringify({ - isReady: this.toolContext!.client.isReady(), - hasToken: !!this.toolContext!.client.token, - user: this.toolContext!.client.user ? { - id: this.toolContext!.client.user.id, - tag: this.toolContext!.client.user.tag, - } : null - })}`); - - // Check if we have a token but not ready - try to force reconnect - if (this.toolContext!.client.token) { - info("Has token but not ready - attempting to force reconnect"); - try { - // Attempt to force login with existing token - await this.toolContext!.client.login(this.toolContext!.client.token); - info(`Force reconnect successful: ${this.toolContext!.client.isReady()}`); - - // If still not ready after reconnect, return error - if (!this.toolContext!.client.isReady()) { - return res.json({ - jsonrpc: '2.0', - error: { - code: -32603, - message: 'Discord client reconnect failed. Please use discord_login tool first.', - }, - id: req.body?.id || null, - }); - } - - // Continue with original request as now logged in - info("Reconnected successfully, continuing with original request"); - } catch (reconnectError) { - error(`Reconnect failed: ${reconnectError instanceof Error ? reconnectError.message : String(reconnectError)}`); - return res.json({ - jsonrpc: '2.0', - error: { - code: -32603, - message: 'Discord client reconnect failed. Please use discord_login tool first.', - }, - id: req.body?.id || null, - }); - } - } else { - return res.json({ - jsonrpc: '2.0', - error: { - code: -32603, - message: 'Discord client not logged in. Please use discord_login tool first.', - }, - id: req.body?.id || null, - }); - } - } - - // Call appropriate handler based on method - switch (method) { - case 'discord_send': - result = await sendMessageHandler(params, this.toolContext!); - break; - case 'discord_get_forum_channels': - result = await getForumChannelsHandler(params, this.toolContext!); - break; - case 'discord_create_forum_post': - result = await createForumPostHandler(params, this.toolContext!); - break; - case 'discord_get_forum_post': - result = await getForumPostHandler(params, this.toolContext!); - break; - case 'discord_reply_to_forum': - result = await replyToForumHandler(params, this.toolContext!); - break; - case 'discord_delete_forum_post': - result = await deleteForumPostHandler(params, this.toolContext!); - break; - case 'discord_create_text_channel': - result = await createTextChannelHandler(params, this.toolContext!); - break; - case 'discord_delete_channel': - result = await deleteChannelHandler(params, this.toolContext!); - break; - case 'discord_read_messages': - result = await readMessagesHandler(params, this.toolContext!); - break; - case 'discord_get_server_info': - result = await getServerInfoHandler(params, this.toolContext!); - break; - case 'discord_add_reaction': - result = await addReactionHandler(params, this.toolContext!); - break; - case 'discord_add_multiple_reactions': - result = await addMultipleReactionsHandler(params, this.toolContext!); - break; - case 'discord_remove_reaction': - result = await removeReactionHandler(params, this.toolContext!); - break; - case 'discord_delete_message': - result = await deleteMessageHandler(params, this.toolContext!); - break; - case 'discord_create_webhook': - result = await createWebhookHandler(params, this.toolContext!); - break; - case 'discord_send_webhook_message': - result = await sendWebhookMessageHandler(params, this.toolContext!); - break; - case 'discord_edit_webhook': - result = await editWebhookHandler(params, this.toolContext!); - break; - case 'discord_delete_webhook': - result = await deleteWebhookHandler(params, this.toolContext!); - break; - case 'discord_create_category': - result = await createCategoryHandler(params, this.toolContext!); - break; - case 'discord_edit_category': - result = await editCategoryHandler(params, this.toolContext!); - break; - case 'discord_delete_category': - result = await deleteCategoryHandler(params, this.toolContext!); - break; - case 'discord_list_servers': - result = await listServersHandler(params, this.toolContext!); - case 'discord_search_messages': - result = await searchMessagesHandler(params, this.toolContext!); - break;break; - } - break; - - - - case 'tools/call': - // Handle new tools/call method format - const toolName = params.name; - const toolArgs = params.arguments || {}; - - // Check if Discord client is logged in for Discord API tools - if (toolName !== 'discord_login' && - toolName.startsWith('discord_') && - !this.toolContext!.client.isReady()) { - error(`Client not ready for tool ${toolName}, client state: ${JSON.stringify({ - isReady: this.toolContext!.client.isReady(), - hasToken: !!this.toolContext!.client.token, - user: this.toolContext!.client.user ? { - id: this.toolContext!.client.user.id, - tag: this.toolContext!.client.user.tag, - } : null - })}`); - - // Check if we have a token but not ready - try to force reconnect - if (this.toolContext!.client.token) { - info("Has token but not ready - attempting to force reconnect"); - try { - // Attempt to force login with existing token - await this.toolContext!.client.login(this.toolContext!.client.token); - info(`Force reconnect successful: ${this.toolContext!.client.isReady()}`); - - // If still not ready after reconnect, return error - if (!this.toolContext!.client.isReady()) { - return res.json({ - jsonrpc: '2.0', - error: { - code: -32603, - message: 'Discord client reconnect failed. Please use discord_login tool first.', - }, - id: req.body?.id || null, - }); - } - - // Continue with original request as now logged in - info("Reconnected successfully, continuing with original request"); - } catch (reconnectError) { - error(`Reconnect failed: ${reconnectError instanceof Error ? reconnectError.message : String(reconnectError)}`); - return res.json({ - jsonrpc: '2.0', - error: { - code: -32603, - message: 'Discord client reconnect failed. Please use discord_login tool first.', - }, - id: req.body?.id || null, - }); - } - } else { - return res.json({ - jsonrpc: '2.0', - error: { - code: -32603, - message: 'Discord client not logged in. Please use discord_login tool first.', - }, - id: req.body?.id || null, - }); - } - } - - // Call the appropriate handler based on tool name - switch (toolName) { - case 'discord_login': - result = await loginHandler(toolArgs, this.toolContext!); - // Log client state after login - info(`Client state after login: ${JSON.stringify({ - isReady: this.toolContext!.client.isReady(), - hasToken: !!this.toolContext!.client.token, - user: this.toolContext!.client.user ? { - id: this.toolContext!.client.user.id, - tag: this.toolContext!.client.user.tag, - } : null - })}`); - break; - - case 'discord_send': - result = await sendMessageHandler(toolArgs, this.toolContext!); - break; - - case 'discord_get_forum_channels': - result = await getForumChannelsHandler(toolArgs, this.toolContext!); - break; - - case 'discord_create_forum_post': - result = await createForumPostHandler(toolArgs, this.toolContext!); - break; - - case 'discord_get_forum_post': - result = await getForumPostHandler(toolArgs, this.toolContext!); - break; - - case 'discord_reply_to_forum': - result = await replyToForumHandler(toolArgs, this.toolContext!); - break; - - case 'discord_delete_forum_post': - result = await deleteForumPostHandler(toolArgs, this.toolContext!); - break; - - case 'discord_create_text_channel': - result = await createTextChannelHandler(toolArgs, this.toolContext!); - break; - - case 'discord_delete_channel': - result = await deleteChannelHandler(toolArgs, this.toolContext!); - break; - - case 'discord_read_messages': - result = await readMessagesHandler(toolArgs, this.toolContext!); - break; - - case 'discord_get_server_info': - result = await getServerInfoHandler(toolArgs, this.toolContext!); - break; - - case 'discord_add_reaction': - result = await addReactionHandler(toolArgs, this.toolContext!); - break; - - case 'discord_add_multiple_reactions': - result = await addMultipleReactionsHandler(toolArgs, this.toolContext!); - break; - - case 'discord_remove_reaction': - result = await removeReactionHandler(toolArgs, this.toolContext!); - break; - - case 'discord_delete_message': - result = await deleteMessageHandler(toolArgs, this.toolContext!); - break; - - case 'discord_create_webhook': - result = await createWebhookHandler(toolArgs, this.toolContext!); - break; - - case 'discord_send_webhook_message': - result = await sendWebhookMessageHandler(toolArgs, this.toolContext!); - break; - - case 'discord_edit_webhook': - result = await editWebhookHandler(toolArgs, this.toolContext!); - break; - - case 'discord_delete_webhook': - result = await deleteWebhookHandler(toolArgs, this.toolContext!); - break; - case 'discord_create_category': - result = await createCategoryHandler(toolArgs, this.toolContext!); - break; - case 'discord_edit_category': - result = await editCategoryHandler(toolArgs, this.toolContext!); - break; - case 'discord_delete_category': - result = await deleteCategoryHandler(toolArgs, this.toolContext!); - break; - - case 'discord_list_servers': - result = await listServersHandler(toolArgs, this.toolContext!); - break; - case 'discord_search_messages': - result = await searchMessagesHandler(toolArgs, this.toolContext!); - break; - - default: - return res.status(400).json({ - jsonrpc: '2.0', - error: { - code: -32601, - message: `Unknown tool: ${toolName}`, - }, - id: req.body?.id || null, - }); - } - break; - - default: - // For method 'ping' and other non-critical methods, just return an empty result - // This ensures MCP compatibility for health checks and probes - if (method === 'ping') { - info(`Returning empty response for ping request`); - result = {}; - } else { - // For other unknown methods, return method not found error - return res.status(400).json({ - jsonrpc: '2.0', - error: { - code: -32601, - message: `Method not found: ${method}`, - }, - id: req.body?.id || null, - }); - } - } - - info(`Request for ${method} handled successfully`); - - // Handle the case where tool handlers return { content, isError } - if (result && typeof result === 'object' && 'content' in result) { - // If it's an error from the tool handler - if ('isError' in result && result.isError) { - error(`Tool error response: ${JSON.stringify(result)}`); - return res.json({ - jsonrpc: '2.0', - id: req.body.id, - error: { - code: -32603, - message: Array.isArray(result.content) - ? result.content.map((item: any) => item.text).join(' ') - : 'Tool execution error' - } - }); - } - - // Return success result but maintain same format as other RPC methods - const finalResponse = { - jsonrpc: '2.0', - id: req.body.id, - result: result - }; - info(`Sending response (session ${this.sessionId}): ${JSON.stringify(finalResponse)}`); - return res.json(finalResponse); - } - - // Standard result format - const finalResponse = { - jsonrpc: '2.0', - id: req.body.id, - result: result - }; - info(`Sending response (session ${this.sessionId}): ${JSON.stringify(finalResponse)}`); - return res.json(finalResponse); - - } catch (err) { - error('Error processing tool request: ' + String(err)); - // Handle validation errors - if (err && typeof err === 'object' && 'name' in err && err.name === 'ZodError') { - return res.json({ - jsonrpc: '2.0', - error: { - code: -32602, - message: `Invalid parameters: ${err && typeof err === 'object' && 'message' in err ? String((err as any).message) : 'Unknown validation error'}`, - }, - id: req.body?.id || null, - }); - } - // Handle all other errors - return res.json({ - jsonrpc: '2.0', - error: { - code: -32603, - message: err instanceof Error ? err.message : 'Unknown error', - }, - id: req.body?.id || null, - }); - } - - } catch (err) { - error('Error handling MCP request: ' + String(err)); - if (!res.headersSent) { - res.json({ - jsonrpc: '2.0', - error: { - code: -32603, - message: err instanceof Error ? err.message : 'Internal server error', - }, - id: req.body?.id || null, - }); - } - } - } - async start(server: Server): Promise { this.server = server; info('Starting HTTP transport with server: ' + String(!!this.server)); // Try to get client from the DiscordMCPServer instance - // First, check if the server is passed from DiscordMCPServer if (server) { - // Try to access client directly from server._context const anyServer = server as any; let client: Client | undefined; @@ -637,12 +82,10 @@ export class StreamableHttpTransport implements MCPTransport { client = anyServer._context.client; info('Found client in server._context'); } - // Also check if the server object has client directly else if (anyServer.client instanceof Client) { client = anyServer.client; info('Found client directly on server object'); } - // Look in parent object if available else if (anyServer._parent?.client instanceof Client) { client = anyServer._parent.client; info('Found client in server._parent'); @@ -652,7 +95,6 @@ export class StreamableHttpTransport implements MCPTransport { this.toolContext = createToolContext(client); info('Tool context initialized with Discord client'); } else { - // Create a real Discord client instead of a dummy info('Creating new Discord client for transport'); const newClient = new Client({ intents: [ @@ -667,15 +109,27 @@ export class StreamableHttpTransport implements MCPTransport { } } - // Create a stateless transport + // Create transport this.transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: undefined // set to undefined for stateless servers + sessionIdGenerator: undefined // stateless }); // Connect the transport await this.server.connect(this.transport); info('Transport connected'); + // Setup /mcp endpoint to use transport.handleRequest() + this.app.all('/mcp', async (req: Request, res: Response) => { + try { + await this.transport!.handleRequest(req, res, req.body); + } catch (err) { + error('Error handling MCP request: ' + String(err)); + if (!res.headersSent) { + res.status(500).json({ error: 'Internal server error' }); + } + } + }); + return new Promise((resolve) => { this.httpServer = this.app.listen(this.port, '0.0.0.0', () => { info(`MCP Server listening on 0.0.0.0:${this.port}`); From 8021295cbbffdeea3ddf4d22392c684c9dc9188f Mon Sep 17 00:00:00 2001 From: LiQiuDGG Date: Tue, 13 Jan 2026 09:56:37 -0500 Subject: [PATCH 4/4] feat: add discord_list_forum_threads tool Add new tool to list all threads (posts) in a Discord forum channel, including both active and archived threads. This addresses the limitation where discord_search_messages only returns 25 message results and doesn't provide a complete thread listing. - Fetch active threads via channel.threads.fetchActive() - Optionally fetch archived threads via channel.threads.fetchArchived() - Deduplicate and sort threads by creation date (newest first) - Return thread metadata including id, name, creator, dates, message count --- src/schemas.ts | 6 +++ src/server.ts | 6 +++ src/toolList.ts | 13 +++++++ src/tools/forum.ts | 95 +++++++++++++++++++++++++++++++++++++++++++++- src/tools/tools.ts | 10 +++-- 5 files changed, 125 insertions(+), 5 deletions(-) diff --git a/src/schemas.ts b/src/schemas.ts index fabc61d..08f6a52 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -25,6 +25,12 @@ export const GetForumPostSchema = z.object({ threadId: z.string() }); +export const ListForumThreadsSchema = z.object({ + forumChannelId: z.string(), + includeArchived: z.boolean().optional().default(true), + limit: z.number().min(1).max(100).optional().default(100) +}); + export const ReplyToForumSchema = z.object({ threadId: z.string(), message: z.string() diff --git a/src/server.ts b/src/server.ts index d086f86..535349b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -13,6 +13,7 @@ import { getForumChannelsHandler, createForumPostHandler, getForumPostHandler, + listForumThreadsHandler, replyToForumHandler, deleteForumPostHandler, createTextChannelHandler, @@ -110,6 +111,11 @@ export class DiscordMCPServer { toolResponse = await getForumPostHandler(args, this.toolContext); return toolResponse; + case "discord_list_forum_threads": + this.logClientState("before discord_list_forum_threads handler"); + toolResponse = await listForumThreadsHandler(args, this.toolContext); + return toolResponse; + case "discord_reply_to_forum": this.logClientState("before discord_reply_to_forum handler"); toolResponse = await replyToForumHandler(args, this.toolContext); diff --git a/src/toolList.ts b/src/toolList.ts index 4ae460d..2fc7864 100644 --- a/src/toolList.ts +++ b/src/toolList.ts @@ -102,6 +102,19 @@ export const toolList = [ required: ["threadId"] } }, + { + name: "discord_list_forum_threads", + description: "Lists all threads (posts) in a Discord forum channel, including both active and archived threads", + inputSchema: { + type: "object", + properties: { + forumChannelId: { type: "string", description: "The ID of the forum channel to list threads from" }, + includeArchived: { type: "boolean", description: "Whether to include archived threads (default: true)", default: true }, + limit: { type: "number", description: "Maximum number of archived threads to fetch (default: 100, max: 100)", minimum: 1, maximum: 100, default: 100 } + }, + required: ["forumChannelId"] + } + }, { name: "discord_reply_to_forum", description: "Adds a reply to an existing forum post or thread", diff --git a/src/tools/forum.ts b/src/tools/forum.ts index 8af3a24..8fec4b0 100644 --- a/src/tools/forum.ts +++ b/src/tools/forum.ts @@ -1,5 +1,5 @@ import { ChannelType, ForumChannel } from 'discord.js'; -import { GetForumChannelsSchema, CreateForumPostSchema, GetForumPostSchema, ReplyToForumSchema, DeleteForumPostSchema } from '../schemas.js'; +import { GetForumChannelsSchema, CreateForumPostSchema, GetForumPostSchema, ListForumThreadsSchema, ReplyToForumSchema, DeleteForumPostSchema } from '../schemas.js'; import { ToolHandler } from './types.js'; import { handleDiscordError } from "../errorHandler.js"; @@ -145,6 +145,99 @@ export const getForumPostHandler: ToolHandler = async (args, { client }) => { } }; +export const listForumThreadsHandler: ToolHandler = async (args, { client }) => { + const { forumChannelId, includeArchived, limit } = ListForumThreadsSchema.parse(args); + + try { + if (!client.isReady()) { + return { + content: [{ type: "text", text: "Discord client not logged in." }], + 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; + + // Fetch active threads + const activeThreads = await forumChannel.threads.fetchActive(); + + // Fetch archived threads if requested + let archivedThreads: typeof activeThreads | null = null; + if (includeArchived) { + archivedThreads = await forumChannel.threads.fetchArchived({ limit: limit }); + } + + // Combine and format thread information + const threads: Array<{ + id: string; + name: string; + createdAt: Date | null; + archived: boolean; + locked: boolean; + messageCount: number | null; + ownerId: string | null; + }> = []; + + // Add active threads + activeThreads.threads.forEach(thread => { + threads.push({ + id: thread.id, + name: thread.name, + createdAt: thread.createdAt, + archived: thread.archived || false, + locked: thread.locked || false, + messageCount: thread.messageCount, + ownerId: thread.ownerId + }); + }); + + // Add archived threads if fetched + if (archivedThreads) { + archivedThreads.threads.forEach(thread => { + // Avoid duplicates + if (!threads.find(t => t.id === thread.id)) { + threads.push({ + id: thread.id, + name: thread.name, + createdAt: thread.createdAt, + archived: thread.archived || false, + locked: thread.locked || false, + messageCount: thread.messageCount, + ownerId: thread.ownerId + }); + } + }); + } + + // Sort by creation date (newest first) + threads.sort((a, b) => { + if (!a.createdAt || !b.createdAt) return 0; + return b.createdAt.getTime() - a.createdAt.getTime(); + }); + + return { + content: [{ + type: "text", + text: JSON.stringify({ + forumChannelId, + totalThreads: threads.length, + threads + }, null, 2) + }] + }; + } catch (error) { + return handleDiscordError(error); + } +}; + export const replyToForumHandler: ToolHandler = async (args, { client }) => { const { threadId, message } = ReplyToForumSchema.parse(args); diff --git a/src/tools/tools.ts b/src/tools/tools.ts index 78e74c0..a35a785 100644 --- a/src/tools/tools.ts +++ b/src/tools/tools.ts @@ -3,10 +3,11 @@ import { z } from "zod"; import { ToolResponse, ToolContext, ToolHandler } from "./types.js"; import { loginHandler } from './login.js'; import { sendMessageHandler } from './send-message.js'; -import { - getForumChannelsHandler, - createForumPostHandler, - getForumPostHandler, +import { + getForumChannelsHandler, + createForumPostHandler, + getForumPostHandler, + listForumThreadsHandler, replyToForumHandler, deleteForumPostHandler } from './forum.js'; @@ -43,6 +44,7 @@ export { getForumChannelsHandler, createForumPostHandler, getForumPostHandler, + listForumThreadsHandler, replyToForumHandler, deleteForumPostHandler, createTextChannelHandler,