fix: Fix Discord client login state handling and improve error reporting
This commit is contained in:
parent
73eee8a5cf
commit
944af5def9
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
};
|
203
src/transport.ts
203
src/transport.ts
|
@ -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,
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue