Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions pkg/handler/conversations.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,24 @@ type filesGetParams struct {
fileID string
}

type reactionsListParams struct {
count int
page int
full bool
}

type ReactedItemRow struct {
Type string `csv:"type"`
Channel string `csv:"channel"`
Timestamp string `csv:"timestamp"`
Emoji string `csv:"emoji"`
Text string `csv:"text"`
UserID string `csv:"userID"`
UserName string `csv:"userName"`
Time string `csv:"time"`
Cursor string `csv:"cursor"`
}

type ConversationsHandler struct {
apiProvider *provider.ApiProvider
logger *zap.Logger
Expand Down Expand Up @@ -324,6 +342,120 @@ func (ch *ConversationsHandler) ReactionsRemoveHandler(ctx context.Context, requ
return mcp.NewToolResultText(fmt.Sprintf("Successfully removed :%s: reaction from message %s in channel %s", params.emoji, params.timestamp, params.channel)), nil
}

// ReactionsListHandler lists items the authenticated user has reacted to
func (ch *ConversationsHandler) ReactionsListHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
ch.logger.Debug("ReactionsListHandler called", zap.Any("params", request.Params))

// provider readiness
if ready, err := ch.apiProvider.IsReady(); !ready {
ch.logger.Error("API provider not ready", zap.Error(err))
return nil, err
}

params, err := ch.parseParamsToolReactionsList(request)
if err != nil {
ch.logger.Error("Failed to parse reactions_list params", zap.Error(err))
return nil, err
}

listParams := slack.ListReactionsParameters{
Count: params.count,
Page: params.page,
Full: params.full,
}

ch.logger.Debug("Listing reactions",
zap.Int("count", params.count),
zap.Int("page", params.page),
zap.Bool("full", params.full),
)

reactedItems, paging, err := ch.apiProvider.Slack().ListReactionsContext(ctx, listParams)
if err != nil {
ch.logger.Error("Slack ListReactionsContext failed", zap.Error(err))
return nil, err
}
ch.logger.Debug("Fetched reacted items", zap.Int("count", len(reactedItems)))

rows := ch.convertReactedItems(reactedItems)

// Add pagination cursor to the last row if there are more pages
if len(rows) > 0 && paging != nil && paging.Page < paging.Pages {
rows[len(rows)-1].Cursor = fmt.Sprintf("%d", paging.Page+1)
}

return marshalReactedItemsToCSV(rows)
}

func (ch *ConversationsHandler) convertReactedItems(items []slack.ReactedItem) []ReactedItemRow {
usersMap := ch.apiProvider.ProvideUsersMap()
var rows []ReactedItemRow

for _, item := range items {
// Get message text and user info
var msgText, userID, userName, timestamp, channel string
channel = item.Channel

switch item.Type {
case "message":
if item.Message != nil {
msgText = item.Message.Text
userID = item.Message.User
timestamp = item.Message.Timestamp
}
case "file":
if item.File != nil {
msgText = item.File.Name
userID = item.File.User
timestamp = item.File.Timestamp.String()
}
case "file_comment":
if item.Comment != nil {
msgText = item.Comment.Comment
userID = item.Comment.User
timestamp = fmt.Sprintf("%d", item.Comment.Timestamp)
}
}

// Resolve username
if u, ok := usersMap.Users[userID]; ok {
userName = u.Name
} else {
userName = userID
}

// Convert timestamp to readable format
timeStr, err := text.TimestampToIsoRFC3339(timestamp)
if err != nil {
timeStr = timestamp
}

// Create a row for each reaction on this item
for _, reaction := range item.Reactions {
rows = append(rows, ReactedItemRow{
Type: item.Type,
Channel: channel,
Timestamp: timestamp,
Emoji: reaction.Name,
Text: text.ProcessText(msgText),
UserID: userID,
UserName: userName,
Time: timeStr,
})
}
}

return rows
}

func marshalReactedItemsToCSV(rows []ReactedItemRow) (*mcp.CallToolResult, error) {
csvBytes, err := gocsv.MarshalBytes(&rows)
if err != nil {
return nil, err
}
return mcp.NewToolResultText(string(csvBytes)), nil
}

func (ch *ConversationsHandler) FilesGetHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
ch.logger.Debug("FilesGetHandler called", zap.Any("params", request.Params))

Expand Down Expand Up @@ -842,6 +974,29 @@ func (ch *ConversationsHandler) parseParamsToolReaction(request mcp.CallToolRequ
}, nil
}

func (ch *ConversationsHandler) parseParamsToolReactionsList(request mcp.CallToolRequest) (*reactionsListParams, error) {
count := request.GetInt("count", 100)
if count < 1 {
count = 100
}
if count > 1000 {
count = 1000
}

page := request.GetInt("page", 1)
if page < 1 {
page = 1
}

full := request.GetBool("full", true)

return &reactionsListParams{
count: count,
page: page,
full: full,
}, nil
}

func (ch *ConversationsHandler) parseParamsToolFilesGet(request mcp.CallToolRequest) (*filesGetParams, error) {
toolConfig := os.Getenv("SLACK_MCP_ATTACHMENT_TOOL")
if toolConfig == "" {
Expand Down
5 changes: 5 additions & 0 deletions pkg/provider/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ type SlackAPI interface {
MarkConversationContext(ctx context.Context, channel, ts string) error
AddReactionContext(ctx context.Context, name string, item slack.ItemRef) error
RemoveReactionContext(ctx context.Context, name string, item slack.ItemRef) error
ListReactionsContext(ctx context.Context, params slack.ListReactionsParameters) ([]slack.ReactedItem, *slack.Paging, error)

// Used to get messages
GetConversationHistoryContext(ctx context.Context, params *slack.GetConversationHistoryParameters) (*slack.GetConversationHistoryResponse, error)
Expand Down Expand Up @@ -301,6 +302,10 @@ func (c *MCPSlackClient) RemoveReactionContext(ctx context.Context, name string,
return c.slackClient.RemoveReactionContext(ctx, name, item)
}

func (c *MCPSlackClient) ListReactionsContext(ctx context.Context, params slack.ListReactionsParameters) ([]slack.ReactedItem, *slack.Paging, error) {
return c.slackClient.ListReactionsContext(ctx, params)
}

func (c *MCPSlackClient) GetFileInfoContext(ctx context.Context, fileID string, count, page int) (*slack.File, []slack.Comment, *slack.Paging, error) {
return c.slackClient.GetFileInfoContext(ctx, fileID, count, page)
}
Expand Down
18 changes: 18 additions & 0 deletions pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,24 @@ func NewMCPServer(provider *provider.ApiProvider, logger *zap.Logger) *MCPServer
),
), conversationsHandler.ReactionsRemoveHandler)

s.AddTool(mcp.NewTool("reactions_list",
mcp.WithDescription("List items (messages, files) the authenticated user has reacted to. Useful as a workaround for accessing saved/bookmarked items."),
mcp.WithTitleAnnotation("List My Reactions"),
mcp.WithReadOnlyHintAnnotation(true),
mcp.WithNumber("count",
mcp.DefaultNumber(100),
mcp.Description("Number of items to return per page (default 100, max 1000)."),
),
mcp.WithNumber("page",
mcp.DefaultNumber(1),
mcp.Description("Page number for pagination (default 1)."),
),
mcp.WithBoolean("full",
mcp.DefaultBool(true),
mcp.Description("If true, return full message/file details. Default is true."),
),
), conversationsHandler.ReactionsListHandler)

s.AddTool(mcp.NewTool("attachment_get_data",
mcp.WithDescription("Download an attachment's content by file ID. Returns file metadata and content (text files as-is, binary files as base64). Maximum file size is 5MB."),
mcp.WithTitleAnnotation("Get Attachment Data"),
Expand Down