fix: Fix Discord client login state handling and improve error reporting

This commit is contained in:
BarryY 2025-05-15 12:59:38 +08:00
parent 73eee8a5cf
commit 944af5def9
3 changed files with 231 additions and 70 deletions

View File

@ -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<typeof createToolContext>;
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();
}
}

View File

@ -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);
}
};

View File

@ -55,11 +55,14 @@ export class StreamableHttpTransport implements MCPTransport {
private httpServer: any = null;
private transport: StreamableHTTPServerTransport | null = null;
private toolContext: ReturnType<typeof createToolContext> | 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,
});