Skip to content

Commit 9844d5b

Browse files
authored
Merge pull request #141 from xav-ie/feat-add-reactions
feat-add-reactions
2 parents 2572a86 + af19d5e commit 9844d5b

File tree

4 files changed

+139
-9
lines changed

4 files changed

+139
-9
lines changed

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,16 @@ Get list of channels
8484
- `limit` (number, default: 100): The maximum number of items to return. Must be an integer between 1 and 1000 (maximum 999).
8585
- `cursor` (string, optional): Cursor for pagination. Use the value of the last row and column in the response as next_cursor field returned from the previous request.
8686

87+
### 6. reactions_add:
88+
Add an emoji reaction to a message in a public channel, private channel, or direct message (DM, or IM) conversation.
89+
90+
> **Note:** Adding reactions is disabled by default for safety. To enable, set the `SLACK_MCP_ADD_MESSAGE_TOOL` environment variable. If set to a comma-separated list of channel IDs, reactions are enabled only for those specific channels. See the Environment Variables section below for details.
91+
92+
- **Parameters:**
93+
- `channel_id` (string, required): ID of the channel in format `Cxxxxxxxxxx` or its name starting with `#...` or `@...` aka `#general` or `@username_dm`.
94+
- `timestamp` (string, required): Timestamp of the message to add reaction to, in format `1234567890.123456`.
95+
- `emoji` (string, required): The name of the emoji to add as a reaction (without colons). Example: `thumbsup`, `heart`, `rocket`.
96+
8797
## Resources
8898

8999
The Slack MCP Server exposes two special directory resources for easy access to workspace metadata:
@@ -135,7 +145,7 @@ Fetches a CSV directory of all users in the workspace.
135145
| `SLACK_MCP_SERVER_CA` | No | `nil` | Path to CA certificate |
136146
| `SLACK_MCP_SERVER_CA_TOOLKIT` | No | `nil` | Inject HTTPToolkit CA certificate to root trust-store for MitM debugging |
137147
| `SLACK_MCP_SERVER_CA_INSECURE` | No | `false` | Trust all insecure requests (NOT RECOMMENDED) |
138-
| `SLACK_MCP_ADD_MESSAGE_TOOL` | No | `nil` | Enable message posting via `conversations_add_message` by setting it to true for all channels, a comma-separated list of channel IDs to whitelist specific channels, or use `!` before a channel ID to allow all except specified ones, while an empty value disables posting by default. |
148+
| `SLACK_MCP_ADD_MESSAGE_TOOL` | No | `nil` | Enable message posting via `conversations_add_message` and emoji reactions via `reactions_add` by setting it to true for all channels, a comma-separated list of channel IDs to whitelist specific channels, or use `!` before a channel ID to allow all except specified ones, while an empty value disables these tools by default. |
139149
| `SLACK_MCP_ADD_MESSAGE_MARK` | No | `nil` | When the `conversations_add_message` tool is enabled, any new message sent will automatically be marked as read. |
140150
| `SLACK_MCP_ADD_MESSAGE_UNFURLING` | No | `nil` | Enable to let Slack unfurl posted links or set comma-separated list of domains e.g. `github.com,slack.com` to whitelist unfurling only for them. If text contains whitelisted and unknown domain unfurling will be disabled for security reasons. |
141151
| `SLACK_MCP_USERS_CACHE` | No | `~/Library/Caches/slack-mcp-server/users_cache.json` (macOS)<br>`~/.cache/slack-mcp-server/users_cache.json` (Linux)<br>`%LocalAppData%/slack-mcp-server/users_cache.json` (Windows) | Path to the users cache file. Used to cache Slack user information to avoid repeated API calls on startup. |

pkg/handler/conversations.go

Lines changed: 107 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ type addMessageParams struct {
7979
contentType string
8080
}
8181

82+
type addReactionParams struct {
83+
channel string
84+
timestamp string
85+
emoji string
86+
}
87+
8288
type ConversationsHandler struct {
8389
apiProvider *provider.ApiProvider
8490
logger *zap.Logger
@@ -155,6 +161,12 @@ func (ch *ConversationsHandler) UsersResource(ctx context.Context, request mcp.R
155161
func (ch *ConversationsHandler) ConversationsAddMessageHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
156162
ch.logger.Debug("ConversationsAddMessageHandler called", zap.Any("params", request.Params))
157163

164+
// provider readiness
165+
if ready, err := ch.apiProvider.IsReady(); !ready {
166+
ch.logger.Error("API provider not ready", zap.Error(err))
167+
return nil, err
168+
}
169+
158170
params, err := ch.parseParamsToolAddMessage(request)
159171
if err != nil {
160172
ch.logger.Error("Failed to parse add-message params", zap.Error(err))
@@ -230,6 +242,42 @@ func (ch *ConversationsHandler) ConversationsAddMessageHandler(ctx context.Conte
230242
return marshalMessagesToCSV(messages)
231243
}
232244

245+
// ReactionsAddHandler adds an emoji reaction to a message
246+
func (ch *ConversationsHandler) ReactionsAddHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
247+
ch.logger.Debug("ReactionsAddHandler called", zap.Any("params", request.Params))
248+
249+
// provider readiness
250+
if ready, err := ch.apiProvider.IsReady(); !ready {
251+
ch.logger.Error("API provider not ready", zap.Error(err))
252+
return nil, err
253+
}
254+
255+
params, err := ch.parseParamsToolAddReaction(request)
256+
if err != nil {
257+
ch.logger.Error("Failed to parse add-reaction params", zap.Error(err))
258+
return nil, err
259+
}
260+
261+
itemRef := slack.ItemRef{
262+
Channel: params.channel,
263+
Timestamp: params.timestamp,
264+
}
265+
266+
ch.logger.Debug("Adding reaction to Slack message",
267+
zap.String("channel", params.channel),
268+
zap.String("timestamp", params.timestamp),
269+
zap.String("emoji", params.emoji),
270+
)
271+
272+
err = ch.apiProvider.Slack().AddReactionContext(ctx, params.emoji, itemRef)
273+
if err != nil {
274+
ch.logger.Error("Slack AddReactionContext failed", zap.Error(err))
275+
return nil, err
276+
}
277+
278+
return mcp.NewToolResultText(fmt.Sprintf("Successfully added :%s: reaction to message %s in channel %s", params.emoji, params.timestamp, params.channel)), nil
279+
}
280+
233281
// ConversationsHistoryHandler streams conversation history as CSV
234282
func (ch *ConversationsHandler) ConversationsHistoryHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
235283
ch.logger.Debug("ConversationsHistoryHandler called", zap.Any("params", request.Params))
@@ -363,6 +411,18 @@ func isChannelAllowed(channel string) bool {
363411
return !isNegated
364412
}
365413

414+
func (ch *ConversationsHandler) resolveChannelID(channel string) (string, error) {
415+
if !strings.HasPrefix(channel, "#") && !strings.HasPrefix(channel, "@") {
416+
return channel, nil
417+
}
418+
channelsMaps := ch.apiProvider.ProvideChannelsMaps()
419+
chn, ok := channelsMaps.ChannelsInv[channel]
420+
if !ok {
421+
return "", fmt.Errorf("channel %q not found", channel)
422+
}
423+
return channelsMaps.Channels[chn].ID, nil
424+
}
425+
366426
func (ch *ConversationsHandler) convertMessagesFromHistory(slackMessages []slack.Message, channel string, includeActivity bool) []Message {
367427
usersMap := ch.apiProvider.ProvideUsersMap()
368428
var messages []Message
@@ -552,14 +612,10 @@ func (ch *ConversationsHandler) parseParamsToolAddMessage(request mcp.CallToolRe
552612
ch.logger.Error("channel_id missing in add-message params")
553613
return nil, errors.New("channel_id must be a string")
554614
}
555-
if strings.HasPrefix(channel, "#") || strings.HasPrefix(channel, "@") {
556-
channelsMaps := ch.apiProvider.ProvideChannelsMaps()
557-
chn, ok := channelsMaps.ChannelsInv[channel]
558-
if !ok {
559-
ch.logger.Error("Channel not found", zap.String("channel", channel))
560-
return nil, fmt.Errorf("channel %q not found", channel)
561-
}
562-
channel = channelsMaps.Channels[chn].ID
615+
channel, err := ch.resolveChannelID(channel)
616+
if err != nil {
617+
ch.logger.Error("Channel not found", zap.String("channel", channel), zap.Error(err))
618+
return nil, err
563619
}
564620
if !isChannelAllowed(channel) {
565621
ch.logger.Warn("Add-message tool not allowed for channel", zap.String("channel", channel), zap.String("policy", toolConfig))
@@ -592,6 +648,49 @@ func (ch *ConversationsHandler) parseParamsToolAddMessage(request mcp.CallToolRe
592648
}, nil
593649
}
594650

651+
func (ch *ConversationsHandler) parseParamsToolAddReaction(request mcp.CallToolRequest) (*addReactionParams, error) {
652+
toolConfig := os.Getenv("SLACK_MCP_ADD_MESSAGE_TOOL")
653+
if toolConfig == "" {
654+
ch.logger.Error("Reactions tool disabled by default")
655+
return nil, errors.New(
656+
"by default, the reactions_add tool is disabled to guard Slack workspaces against accidental spamming. " +
657+
"To enable it, set the SLACK_MCP_ADD_MESSAGE_TOOL environment variable to true, 1, or comma separated list of channels " +
658+
"to limit where the MCP can add reactions, e.g. 'SLACK_MCP_ADD_MESSAGE_TOOL=C1234567890,D0987654321', 'SLACK_MCP_ADD_MESSAGE_TOOL=!C1234567890' " +
659+
"to enable all except one or 'SLACK_MCP_ADD_MESSAGE_TOOL=true' for all channels and DMs",
660+
)
661+
}
662+
663+
channel := request.GetString("channel_id", "")
664+
if channel == "" {
665+
return nil, errors.New("channel_id is required")
666+
}
667+
channel, err := ch.resolveChannelID(channel)
668+
if err != nil {
669+
ch.logger.Error("Channel not found", zap.String("channel", channel), zap.Error(err))
670+
return nil, err
671+
}
672+
if !isChannelAllowed(channel) {
673+
ch.logger.Warn("Reactions tool not allowed for channel", zap.String("channel", channel), zap.String("policy", toolConfig))
674+
return nil, fmt.Errorf("reactions_add tool is not allowed for channel %q, applied policy: %s", channel, toolConfig)
675+
}
676+
677+
timestamp := request.GetString("timestamp", "")
678+
if timestamp == "" {
679+
return nil, errors.New("timestamp is required")
680+
}
681+
682+
emoji := strings.Trim(request.GetString("emoji", ""), ":")
683+
if emoji == "" {
684+
return nil, errors.New("emoji is required")
685+
}
686+
687+
return &addReactionParams{
688+
channel: channel,
689+
timestamp: timestamp,
690+
emoji: emoji,
691+
}, nil
692+
}
693+
595694
func (ch *ConversationsHandler) parseParamsToolSearch(req mcp.CallToolRequest) (*searchParams, error) {
596695
rawQuery := strings.TrimSpace(req.GetString("search_query", ""))
597696
freeText, filters := splitQuery(rawQuery)

pkg/provider/api.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ type SlackAPI interface {
7676
GetUsersInfo(users ...string) (*[]slack.User, error)
7777
PostMessageContext(ctx context.Context, channel string, options ...slack.MsgOption) (string, string, error)
7878
MarkConversationContext(ctx context.Context, channel, ts string) error
79+
AddReactionContext(ctx context.Context, name string, item slack.ItemRef) error
7980

8081
// Used to get messages
8182
GetConversationHistoryContext(ctx context.Context, params *slack.GetConversationHistoryParameters) (*slack.GetConversationHistoryResponse, error)
@@ -286,6 +287,10 @@ func (c *MCPSlackClient) PostMessageContext(ctx context.Context, channelID strin
286287
return c.slackClient.PostMessageContext(ctx, channelID, options...)
287288
}
288289

290+
func (c *MCPSlackClient) AddReactionContext(ctx context.Context, name string, item slack.ItemRef) error {
291+
return c.slackClient.AddReactionContext(ctx, name, item)
292+
}
293+
289294
func (c *MCPSlackClient) ClientUserBoot(ctx context.Context) (*edge.ClientUserBootResponse, error) {
290295
return c.edgeClient.ClientUserBoot(ctx)
291296
}

pkg/server/server.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,22 @@ func NewMCPServer(provider *provider.ApiProvider, logger *zap.Logger) *MCPServer
9999
),
100100
), conversationsHandler.ConversationsAddMessageHandler)
101101

102+
s.AddTool(mcp.NewTool("reactions_add",
103+
mcp.WithDescription("Add an emoji reaction to a message in a public channel, private channel, or direct message (DM, or IM) conversation."),
104+
mcp.WithString("channel_id",
105+
mcp.Required(),
106+
mcp.Description("ID of the channel in format Cxxxxxxxxxx or its name starting with #... or @... aka #general or @username_dm."),
107+
),
108+
mcp.WithString("timestamp",
109+
mcp.Required(),
110+
mcp.Description("Timestamp of the message to add reaction to, in format 1234567890.123456."),
111+
),
112+
mcp.WithString("emoji",
113+
mcp.Required(),
114+
mcp.Description("The name of the emoji to add as a reaction (without colons). Example: 'thumbsup', 'heart', 'rocket'."),
115+
),
116+
), conversationsHandler.ReactionsAddHandler)
117+
102118
conversationsSearchTool := mcp.NewTool("conversations_search_messages",
103119
mcp.WithDescription("Search messages in a public channel, private channel, or direct message (DM, or IM) conversation using filters. All filters are optional, if not provided then search_query is required."),
104120
mcp.WithTitleAnnotation("Search Messages"),

0 commit comments

Comments
 (0)