From 944af5def91a6d95d78fd1f8f97ad021a432eb09 Mon Sep 17 00:00:00 2001 From: BarryY Date: Thu, 15 May 2025 12:59:38 +0800 Subject: [PATCH] fix: Fix Discord client login state handling and improve error reporting --- src/server.ts | 44 ++++++++++ src/tools/login.ts | 54 ++++++++++-- src/transport.ts | 203 +++++++++++++++++++++++++++++++-------------- 3 files changed, 231 insertions(+), 70 deletions(-) diff --git a/src/server.ts b/src/server.ts index f15c763..5b6b709 100644 --- a/src/server.ts +++ b/src/server.ts @@ -29,10 +29,12 @@ import { deleteWebhookHandler } from './tools/tools.js'; import { MCPTransport } from './transport.js'; +import { info, error } from './logger.js'; export class DiscordMCPServer { private server: Server; private toolContext: ReturnType; + private clientStatusInterval: NodeJS.Timeout | null = null; constructor( private client: Client, @@ -71,77 +73,97 @@ export class DiscordMCPServer { switch (name) { case "discord_login": toolResponse = await loginHandler(args, this.toolContext); + // Check the client state after login + this.logClientState("after discord_login handler"); return toolResponse; case "discord_send": + this.logClientState("before discord_send handler"); toolResponse = await sendMessageHandler(args, this.toolContext); return toolResponse; case "discord_get_forum_channels": + this.logClientState("before discord_get_forum_channels handler"); toolResponse = await getForumChannelsHandler(args, this.toolContext); return toolResponse; case "discord_create_forum_post": + this.logClientState("before discord_create_forum_post handler"); toolResponse = await createForumPostHandler(args, this.toolContext); return toolResponse; case "discord_get_forum_post": + this.logClientState("before discord_get_forum_post handler"); toolResponse = await getForumPostHandler(args, this.toolContext); return toolResponse; case "discord_reply_to_forum": + this.logClientState("before discord_reply_to_forum handler"); toolResponse = await replyToForumHandler(args, this.toolContext); return toolResponse; case "discord_delete_forum_post": + this.logClientState("before discord_delete_forum_post handler"); toolResponse = await deleteForumPostHandler(args, this.toolContext); return toolResponse; case "discord_create_text_channel": + this.logClientState("before discord_create_text_channel handler"); toolResponse = await createTextChannelHandler(args, this.toolContext); return toolResponse; case "discord_delete_channel": + this.logClientState("before discord_delete_channel handler"); toolResponse = await deleteChannelHandler(args, this.toolContext); return toolResponse; case "discord_read_messages": + this.logClientState("before discord_read_messages handler"); toolResponse = await readMessagesHandler(args, this.toolContext); return toolResponse; case "discord_get_server_info": + this.logClientState("before discord_get_server_info handler"); toolResponse = await getServerInfoHandler(args, this.toolContext); return toolResponse; case "discord_add_reaction": + this.logClientState("before discord_add_reaction handler"); toolResponse = await addReactionHandler(args, this.toolContext); return toolResponse; case "discord_add_multiple_reactions": + this.logClientState("before discord_add_multiple_reactions handler"); toolResponse = await addMultipleReactionsHandler(args, this.toolContext); return toolResponse; case "discord_remove_reaction": + this.logClientState("before discord_remove_reaction handler"); toolResponse = await removeReactionHandler(args, this.toolContext); return toolResponse; case "discord_delete_message": + this.logClientState("before discord_delete_message handler"); toolResponse = await deleteMessageHandler(args, this.toolContext); return toolResponse; case "discord_create_webhook": + this.logClientState("before discord_create_webhook handler"); toolResponse = await createWebhookHandler(args, this.toolContext); return toolResponse; case "discord_send_webhook_message": + this.logClientState("before discord_send_webhook_message handler"); toolResponse = await sendWebhookMessageHandler(args, this.toolContext); return toolResponse; case "discord_edit_webhook": + this.logClientState("before discord_edit_webhook handler"); toolResponse = await editWebhookHandler(args, this.toolContext); return toolResponse; case "discord_delete_webhook": + this.logClientState("before discord_delete_webhook handler"); toolResponse = await deleteWebhookHandler(args, this.toolContext); return toolResponse; @@ -170,15 +192,37 @@ export class DiscordMCPServer { }); } + private logClientState(context: string) { + info(`Discord client state [${context}]: ${JSON.stringify({ + isReady: this.client.isReady(), + hasToken: !!this.client.token, + user: this.client.user ? { + id: this.client.user.id, + tag: this.client.user.tag, + } : null + })}`); + } + async start() { // Add client to server context so transport can access it (this.server as any)._context = { client: this.client }; (this.server as any).client = this.client; + // Setup periodic client state logging + this.clientStatusInterval = setInterval(() => { + this.logClientState("periodic check"); + }, 10000); + await this.transport.start(this.server); } async stop() { + // Clear the periodic check interval + if (this.clientStatusInterval) { + clearInterval(this.clientStatusInterval); + this.clientStatusInterval = null; + } + await this.transport.stop(); } } \ No newline at end of file diff --git a/src/tools/login.ts b/src/tools/login.ts index 1f87b4a..c2f7adf 100644 --- a/src/tools/login.ts +++ b/src/tools/login.ts @@ -1,6 +1,7 @@ import { DiscordLoginSchema } from '../schemas.js'; import { ToolHandler } from './types.js'; import { handleDiscordError } from "../errorHandler.js"; +import { info, error } from '../logger.js'; export const loginHandler: ToolHandler = async (args, { client }) => { DiscordLoginSchema.parse(args); @@ -8,17 +9,35 @@ export const loginHandler: ToolHandler = async (args, { client }) => { try { // Check if token is provided in the request const token = args.token; + + // Log initial client state + info(`Login handler called with client state: ${JSON.stringify({ + isReady: client.isReady(), + hasToken: !!client.token, + hasArgsToken: !!token, + user: client.user ? { + id: client.user.id, + tag: client.user.tag, + } : null + })}`); // If token is provided and client is already logged in, logout first if (token && client.isReady()) { const currentBotTag = client.user?.tag || 'Unknown'; + info(`Logging out current client (${currentBotTag}) to switch to new token`); + // Destroy the client connection to logout await client.destroy(); + info('Client destroyed successfully'); + // Set the new token client.token = token; // Login with the new token + info('Attempting login with new token'); await client.login(token); + info(`Login successful, new client user: ${client.user?.tag}`); + return { content: [{ type: "text", text: `Successfully switched from ${currentBotTag} to ${client.user?.tag}` }] }; @@ -26,6 +45,7 @@ export const loginHandler: ToolHandler = async (args, { client }) => { // Check if client is already logged in (and no new token provided) if (client.isReady()) { + info(`Client already logged in as: ${client.user?.tag}`); return { content: [{ type: "text", text: `Already logged in as: ${client.user?.tag}` }] }; @@ -33,22 +53,44 @@ export const loginHandler: ToolHandler = async (args, { client }) => { // If token is provided in the request, use it if (token) { + info('Setting token from request'); client.token = token; + } else { + info('No token in request, checking for existing token'); } // Token needs to be set before login if (!client.token) { + error('No token available for login'); return { content: [{ type: "text", text: "Discord token not configured. Cannot log in. Please provide a token in your request or configure it using environment variables." }], isError: true }; } - await client.login(client.token); - return { - content: [{ type: "text", text: `Successfully logged in to Discord: ${client.user?.tag}` }] - }; - } catch (error) { - return handleDiscordError(error); + info('Attempting login with token'); + try { + await client.login(client.token); + info(`Login successful, client user: ${client.user?.tag}`); + + // Verify client is actually ready + if (!client.isReady()) { + error('Client login completed but client.isReady() returned false'); + return { + content: [{ type: "text", text: "Login completed but client is not in ready state. This may indicate an issue with Discord connectivity." }], + isError: true + }; + } + + return { + content: [{ type: "text", text: `Successfully logged in to Discord: ${client.user?.tag}` }] + }; + } catch (loginError) { + error(`Login attempt failed: ${loginError instanceof Error ? loginError.message : String(loginError)}`); + return handleDiscordError(loginError); + } + } catch (err) { + error(`Error in login handler: ${err instanceof Error ? err.message : String(err)}`); + return handleDiscordError(err); } }; \ No newline at end of file diff --git a/src/transport.ts b/src/transport.ts index 805417a..05a3f43 100644 --- a/src/transport.ts +++ b/src/transport.ts @@ -55,11 +55,14 @@ export class StreamableHttpTransport implements MCPTransport { private httpServer: any = null; private transport: StreamableHTTPServerTransport | null = null; private toolContext: ReturnType | null = null; + private sessionId: string = ''; 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() { @@ -89,7 +92,7 @@ export class StreamableHttpTransport implements MCPTransport { private async handleMcpRequest(req: Request, res: Response) { try { if (!this.server) { - return res.status(500).json({ + return res.json({ jsonrpc: '2.0', error: { code: -32603, @@ -99,7 +102,7 @@ export class StreamableHttpTransport implements MCPTransport { }); } - info('Request body: ' + JSON.stringify(req.body)); + info(`Request body (session ${this.sessionId}): ${JSON.stringify(req.body)}`); // Handle all tool requests in a generic way if (!req.body.method) { @@ -120,7 +123,7 @@ export class StreamableHttpTransport implements MCPTransport { // Make sure toolContext is available for tool methods if (!this.toolContext && method !== 'list_tools' && method !== 'initialize') { - return res.status(500).json({ + return res.status(400).json({ jsonrpc: '2.0', error: { code: -32603, @@ -173,78 +176,114 @@ export class StreamableHttpTransport implements MCPTransport { 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': - 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!); + // 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 + })}`); + 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; + } break; case 'ping': @@ -257,10 +296,41 @@ export class StreamableHttpTransport implements MCPTransport { 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 + })}`); + 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': @@ -364,7 +434,8 @@ export class StreamableHttpTransport implements MCPTransport { if (result && typeof result === 'object' && 'content' in result) { // If it's an error from the tool handler if ('isError' in result && result.isError) { - return res.status(400).json({ + error(`Tool error response: ${JSON.stringify(result)}`); + return res.json({ jsonrpc: '2.0', id: req.body.id, error: { @@ -377,25 +448,29 @@ export class StreamableHttpTransport implements MCPTransport { } // Return success result but maintain same format as other RPC methods - return res.json({ + 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 - return res.json({ + 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.status(400).json({ + return res.json({ jsonrpc: '2.0', error: { code: -32602, @@ -405,7 +480,7 @@ export class StreamableHttpTransport implements MCPTransport { }); } // Handle all other errors - return res.status(500).json({ + return res.json({ jsonrpc: '2.0', error: { code: -32603, @@ -418,11 +493,11 @@ export class StreamableHttpTransport implements MCPTransport { } catch (err) { error('Error handling MCP request: ' + String(err)); if (!res.headersSent) { - res.status(500).json({ + res.json({ jsonrpc: '2.0', error: { code: -32603, - message: 'Internal server error', + message: err instanceof Error ? err.message : 'Internal server error', }, id: req.body?.id || null, });