diff --git a/CHANGELOG.md b/CHANGELOG.md index deaf154..94625c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ### Fixed +- Preserve Discord API errors when MCP message reactions fail instead of reporting them as missing custom emoji. - Bound recent live Discord context passed to Claude so deep-context requests cannot contribute unbounded live-message input. - Support announcement and other guild text-based channels in MCP channel tools. - Reject symbolic-link attachment destinations instead of following them when saving downloads. diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 43f4ef9..f0b044e 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -348,7 +348,7 @@ export function createMcpServer(): Server { try { await targetMsg.react(emoji); - } catch { + } catch (error) { // Try custom guild emoji by name const customEmoji = reactChannel.guild.emojis.cache.find( (e) => e.name?.toLowerCase() === emoji.toLowerCase(), @@ -356,9 +356,7 @@ export function createMcpServer(): Server { if (customEmoji) { await targetMsg.react(customEmoji); } else { - throw new Error( - `Could not find emoji "${emoji}". Use a unicode emoji or a custom emoji name from this server.`, - ); + throw error; } } diff --git a/tests/mcpReactionErrors.test.mjs b/tests/mcpReactionErrors.test.mjs new file mode 100644 index 0000000..35febab --- /dev/null +++ b/tests/mcpReactionErrors.test.mjs @@ -0,0 +1,81 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +import { TextChannel } from "discord.js"; + +const messagesDir = fs.mkdtempSync( + path.join(os.tmpdir(), "claudify-mcp-reaction-errors-"), +); +const previousMessagesDir = process.env.MESSAGES_DIR; +process.env.MESSAGES_DIR = messagesDir; +const [{ client: discordClient }, { createMcpServer }] = await Promise.all([ + import("../build/discord/client.js"), + import("../build/mcp/server.js"), +]); +if (previousMessagesDir === undefined) { + delete process.env.MESSAGES_DIR; +} else { + process.env.MESSAGES_DIR = previousMessagesDir; +} + +test.after(() => fs.rmSync(messagesDir, { recursive: true, force: true })); + +test("react-to-message preserves Discord reaction failures", async (t) => { + const reactionError = new Error("Missing Permissions"); + const guild = { + id: "111111111111111111", + name: "Test Server", + emojis: { cache: { find: () => undefined } }, + }; + const channel = Object.create(TextChannel.prototype); + Object.defineProperties(channel, { + name: { value: "general" }, + guild: { value: guild }, + messages: { + value: { + fetch: async () => ({ + react: async () => { + throw reactionError; + }, + }), + }, + }, + }); + + const originalGuildFetch = discordClient.guilds.fetch; + const originalChannelFetch = discordClient.channels.fetch; + discordClient.guilds.fetch = async () => guild; + discordClient.channels.fetch = async () => channel; + t.after(() => { + discordClient.guilds.fetch = originalGuildFetch; + discordClient.channels.fetch = originalChannelFetch; + }); + + const server = createMcpServer(); + t.after(() => server.close().catch(() => {})); + const callToolHandler = server._requestHandlers.get("tools/call"); + assert.ok(callToolHandler); + + await assert.rejects( + () => + callToolHandler( + { + method: "tools/call", + params: { + name: "react-to-message", + arguments: { + server: guild.id, + channel: "222222222222222222", + messageId: "333333333333333333", + emoji: "👍", + }, + }, + }, + {}, + ), + (error) => error === reactionError, + ); +});