From 01f02a04fc9fd63dc9f8f6b0ef390326f2b4dee4 Mon Sep 17 00:00:00 2001 From: vycdev2 Date: Mon, 10 Aug 2026 07:12:08 +0000 Subject: [PATCH] fix: preserve MCP reaction errors --- CHANGELOG.md | 1 + src/mcp/server.ts | 6 +-- tests/mcpReactionErrors.test.mjs | 81 ++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 tests/mcpReactionErrors.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d927b6..976e99a 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. - Prevent mixed reaction replies from narrating the bot's internal choice to react while preserving natural reaction-plus-text responses. - Run Discord-initiated Claude login in a pseudo-terminal so the CLI accepts submitted OAuth codes. - Isolate saved history and summaries by Discord channel ID in dedicated storage namespaces so same-named channels do not share automatic context. diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 2b4d341..8418c2b 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -254,7 +254,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(), @@ -262,9 +262,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, + ); +});