Merge branch 'main' into refactor/streamline-server
This commit is contained in:
commit
5a0c628f35
19
README.md
19
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 Hub Repository**: [barryy625/mcp-discord](https://hub.docker.com/r/barryy625/mcp-discord)
|
||||||
|
|
||||||
|
Docker container uses `stdio` by default.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Pull the latest image
|
# Pull the latest image
|
||||||
docker pull barryy625/mcp-discord:latest
|
docker pull barryy625/mcp-discord:latest
|
||||||
|
|
||||||
# Run with environment variable
|
# 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
|
# 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:**
|
**Available Tags:**
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,12 @@ export const GetForumPostSchema = z.object({
|
||||||
description: "Get details of a specific forum post (thread) by its ID."
|
description: "Get details of a specific forum post (thread) by its ID."
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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({
|
export const ReplyToForumSchema = z.object({
|
||||||
threadId: z.string({ description: "The ID of the forum thread to reply to." }),
|
threadId: z.string({ description: "The ID of the forum thread to reply to." }),
|
||||||
message: z.string({ description: "The content of the reply message." })
|
message: z.string({ description: "The content of the reply message." })
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
getForumChannelsHandler,
|
getForumChannelsHandler,
|
||||||
createForumPostHandler,
|
createForumPostHandler,
|
||||||
getForumPostHandler,
|
getForumPostHandler,
|
||||||
|
listForumThreadsHandler,
|
||||||
replyToForumHandler,
|
replyToForumHandler,
|
||||||
deleteForumPostHandler,
|
deleteForumPostHandler,
|
||||||
createTextChannelHandler,
|
createTextChannelHandler,
|
||||||
|
|
@ -110,6 +111,11 @@ export class DiscordMCPServer {
|
||||||
toolResponse = await getForumPostHandler(args, this.toolContext);
|
toolResponse = await getForumPostHandler(args, this.toolContext);
|
||||||
return toolResponse;
|
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":
|
case "discord_reply_to_forum":
|
||||||
this.logClientState("before discord_reply_to_forum handler");
|
this.logClientState("before discord_reply_to_forum handler");
|
||||||
toolResponse = await replyToForumHandler(args, this.toolContext);
|
toolResponse = await replyToForumHandler(args, this.toolContext);
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,19 @@ export const toolList = [
|
||||||
required: ["threadId"]
|
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",
|
name: "discord_reply_to_forum",
|
||||||
description: "Adds a reply to an existing forum post or thread",
|
description: "Adds a reply to an existing forum post or thread",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ChannelType, ForumChannel } from 'discord.js';
|
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 { ToolHandler } from './types.js';
|
||||||
import { handleDiscordError } from "../errorHandler.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 }) => {
|
export const replyToForumHandler: ToolHandler = async (args, { client }) => {
|
||||||
const { threadId, message } = ReplyToForumSchema.parse(args);
|
const { threadId, message } = ReplyToForumSchema.parse(args);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
getForumChannelsHandler,
|
getForumChannelsHandler,
|
||||||
createForumPostHandler,
|
createForumPostHandler,
|
||||||
getForumPostHandler,
|
getForumPostHandler,
|
||||||
|
listForumThreadsHandler,
|
||||||
replyToForumHandler,
|
replyToForumHandler,
|
||||||
deleteForumPostHandler
|
deleteForumPostHandler
|
||||||
} from './forum.js';
|
} from './forum.js';
|
||||||
|
|
@ -43,6 +44,7 @@ export {
|
||||||
getForumChannelsHandler,
|
getForumChannelsHandler,
|
||||||
createForumPostHandler,
|
createForumPostHandler,
|
||||||
getForumPostHandler,
|
getForumPostHandler,
|
||||||
|
listForumThreadsHandler,
|
||||||
replyToForumHandler,
|
replyToForumHandler,
|
||||||
deleteForumPostHandler,
|
deleteForumPostHandler,
|
||||||
createTextChannelHandler,
|
createTextChannelHandler,
|
||||||
|
|
|
||||||
574
src/transport.ts
574
src/transport.ts
|
|
@ -65,571 +65,16 @@ export class StreamableHttpTransport implements MCPTransport {
|
||||||
constructor(private port: number = 8080) {
|
constructor(private port: number = 8080) {
|
||||||
this.app = express();
|
this.app = express();
|
||||||
this.app.use(express.json());
|
this.app.use(express.json());
|
||||||
this.setupEndpoints();
|
|
||||||
this.sessionId = `session_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
|
this.sessionId = `session_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
|
||||||
info(`Created HTTP transport with session ID: ${this.sessionId}`);
|
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<void> {
|
async start(server: Server): Promise<void> {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
info('Starting HTTP transport with server: ' + String(!!this.server));
|
info('Starting HTTP transport with server: ' + String(!!this.server));
|
||||||
|
|
||||||
// Try to get client from the DiscordMCPServer instance
|
// Try to get client from the DiscordMCPServer instance
|
||||||
// First, check if the server is passed from DiscordMCPServer
|
|
||||||
if (server) {
|
if (server) {
|
||||||
// Try to access client directly from server._context
|
|
||||||
const anyServer = server as any;
|
const anyServer = server as any;
|
||||||
let client: Client | undefined;
|
let client: Client | undefined;
|
||||||
|
|
||||||
|
|
@ -637,12 +82,10 @@ export class StreamableHttpTransport implements MCPTransport {
|
||||||
client = anyServer._context.client;
|
client = anyServer._context.client;
|
||||||
info('Found client in server._context');
|
info('Found client in server._context');
|
||||||
}
|
}
|
||||||
// Also check if the server object has client directly
|
|
||||||
else if (anyServer.client instanceof Client) {
|
else if (anyServer.client instanceof Client) {
|
||||||
client = anyServer.client;
|
client = anyServer.client;
|
||||||
info('Found client directly on server object');
|
info('Found client directly on server object');
|
||||||
}
|
}
|
||||||
// Look in parent object if available
|
|
||||||
else if (anyServer._parent?.client instanceof Client) {
|
else if (anyServer._parent?.client instanceof Client) {
|
||||||
client = anyServer._parent.client;
|
client = anyServer._parent.client;
|
||||||
info('Found client in server._parent');
|
info('Found client in server._parent');
|
||||||
|
|
@ -652,7 +95,6 @@ export class StreamableHttpTransport implements MCPTransport {
|
||||||
this.toolContext = createToolContext(client);
|
this.toolContext = createToolContext(client);
|
||||||
info('Tool context initialized with Discord client');
|
info('Tool context initialized with Discord client');
|
||||||
} else {
|
} else {
|
||||||
// Create a real Discord client instead of a dummy
|
|
||||||
info('Creating new Discord client for transport');
|
info('Creating new Discord client for transport');
|
||||||
const newClient = new Client({
|
const newClient = new Client({
|
||||||
intents: [
|
intents: [
|
||||||
|
|
@ -667,15 +109,27 @@ export class StreamableHttpTransport implements MCPTransport {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a stateless transport
|
// Create transport
|
||||||
this.transport = new StreamableHTTPServerTransport({
|
this.transport = new StreamableHTTPServerTransport({
|
||||||
sessionIdGenerator: undefined // set to undefined for stateless servers
|
sessionIdGenerator: undefined // stateless
|
||||||
});
|
});
|
||||||
|
|
||||||
// Connect the transport
|
// Connect the transport
|
||||||
await this.server.connect(this.transport);
|
await this.server.connect(this.transport);
|
||||||
info('Transport connected');
|
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) => {
|
return new Promise((resolve) => {
|
||||||
this.httpServer = this.app.listen(this.port, '0.0.0.0', () => {
|
this.httpServer = this.app.listen(this.port, '0.0.0.0', () => {
|
||||||
info(`MCP Server listening on 0.0.0.0:${this.port}`);
|
info(`MCP Server listening on 0.0.0.0:${this.port}`);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue