Skip to content

Commit e1c6f91

Browse files
authored
Merge pull request #13 from korotovsky/conversations-replies-tool
Added conversations_replies tool
2 parents cb55a8c + fc7f711 commit e1c6f91

File tree

3 files changed

+135
-8
lines changed

3 files changed

+135
-8
lines changed

README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,20 @@ Model Context Protocol (MCP) server for Slack Workspaces. This integration suppo
1212
- Get messages from the channel by channelID
1313
- Required inputs:
1414
- `channel_id` (string): ID of the channel in format Cxxxxxxxxxx.
15-
- `cursor` (string): Cursor for pagination. Use the value of the last row and column in the response as next_cursor field returned from the previous request.
15+
- `cursor` (string, default: ""): Cursor for pagination. Use the value of the last row and column in the response as next_cursor field returned from the previous request.
1616
- `limit` (string, default: 28): Limit of messages to fetch.
1717
- Returns: List of messages with timestamps, user IDs, and text content
1818

19-
2. `channels_list`
19+
2. `conversations_replies`
20+
- Get a thread of messages posted to a conversation by channelID and thread_ts
21+
- Required inputs:
22+
- `channel_id` (string): ID of the channel in format Cxxxxxxxxxx.
23+
- `thread_ts` (string): Unique identifier of either a thread’s parent message or a message in the thread. ts must be the timestamp in format 1234567890.123456 of an existing message with 0 or more replies.
24+
- `cursor` (string, default: ""): Cursor for pagination. Use the value of the last row and column in the response as next_cursor field returned from the previous request.
25+
- `limit` (string, default: 28): Limit of messages to fetch.
26+
- Returns: List of replies with timestamps, user IDs, and text content
27+
28+
3. `channels_list`
2029
- Get list of channels
2130
- Required inputs:
2231
- `channel_types` (string): Comma-separated channel types. Allowed values: 'mpim', 'im', 'public_channel', 'private_channel'. Example: 'public_channel,private_channel,im'.

pkg/handler/conversations.go

Lines changed: 104 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type Message struct {
2020
UserName string `json:"userUser"`
2121
RealName string `json:"realName"`
2222
Channel string `json:"channelID"`
23+
ThreadTs string `json:"ThreadTs"`
2324
Text string `json:"text"`
2425
Time string `json:"time"`
2526
Cursor string `json:"cursor"`
@@ -87,17 +88,24 @@ func (ch *ConversationsHandler) ConversationsHistoryHandler(ctx context.Context,
8788
for _, message := range messages.Messages {
8889
textTokenized := text.ProcessText(message.Text)
8990
user, ok := usersMap[message.User]
90-
if !ok {
91-
// TODO: add periodic refetch of users
92-
continue
91+
92+
var (
93+
userName = message.User
94+
realName = message.User
95+
)
96+
97+
if ok {
98+
userName = user.Name
99+
realName = user.RealName
93100
}
94101

95102
messageList = append(messageList, Message{
96103
UserID: message.User,
97-
UserName: user.Name,
98-
RealName: user.RealName,
104+
UserName: userName,
105+
RealName: realName,
99106
Text: textTokenized,
100107
Channel: channel,
108+
ThreadTs: message.ThreadTimestamp,
101109
Time: message.Timestamp,
102110
})
103111
}
@@ -114,6 +122,97 @@ func (ch *ConversationsHandler) ConversationsHistoryHandler(ctx context.Context,
114122
return mcp.NewToolResultText(string(csvBytes)), nil
115123
}
116124

125+
func (ch *ConversationsHandler) ConversationsRepliesHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
126+
var (
127+
err error
128+
paramLimit int
129+
paramOldest string
130+
paramLatest string
131+
)
132+
133+
channel := request.GetString("channel_id", "")
134+
if channel == "" {
135+
return nil, errors.New("channel_id must be a string")
136+
}
137+
138+
threadTs := request.GetString("thread_ts", "")
139+
if threadTs == "" {
140+
return nil, errors.New("thread_ts must be a string")
141+
}
142+
143+
limit := request.GetString("limit", "")
144+
cursor := request.GetString("cursor", "")
145+
146+
if strings.HasSuffix(limit, "d") {
147+
paramLimit, paramOldest, paramLatest, err = limitByDays(limit)
148+
if err != nil {
149+
return nil, err
150+
}
151+
} else if cursor == "" {
152+
paramLimit, err = limitByNumeric(limit)
153+
if err != nil {
154+
return nil, err
155+
}
156+
}
157+
158+
api, err := ch.apiProvider.Provide()
159+
if err != nil {
160+
return nil, err
161+
}
162+
163+
params := slack.GetConversationRepliesParameters{
164+
ChannelID: channel,
165+
Timestamp: threadTs,
166+
Limit: paramLimit,
167+
Oldest: paramOldest,
168+
Latest: paramLatest,
169+
Cursor: cursor,
170+
Inclusive: false,
171+
}
172+
messages, hasMore, nextCursor, err := api.GetConversationRepliesContext(ctx, &params)
173+
if err != nil {
174+
return nil, err
175+
}
176+
177+
usersMap := ch.apiProvider.ProvideUsersMap()
178+
179+
var messageList []Message
180+
for _, message := range messages {
181+
textTokenized := text.ProcessText(message.Text)
182+
user, ok := usersMap[message.User]
183+
184+
var (
185+
userName = message.User
186+
realName = message.User
187+
)
188+
189+
if ok {
190+
userName = user.Name
191+
realName = user.RealName
192+
}
193+
194+
messageList = append(messageList, Message{
195+
UserID: message.User,
196+
UserName: userName,
197+
RealName: realName,
198+
Text: textTokenized,
199+
Channel: channel,
200+
Time: message.Timestamp,
201+
})
202+
}
203+
204+
if len(messageList) > 0 && hasMore {
205+
messageList[len(messageList)-1].Cursor = nextCursor
206+
}
207+
208+
csvBytes, err := gocsv.MarshalBytes(&messageList)
209+
if err != nil {
210+
return nil, err
211+
}
212+
213+
return mcp.NewToolResultText(string(csvBytes)), nil
214+
}
215+
117216
func limitByNumeric(limit string) (int, error) {
118217
n, err := strconv.Atoi(limit)
119218
if err != nil {

pkg/server/server.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ type MCPServer struct {
1616
func NewMCPServer(provider *provider.ApiProvider) *MCPServer {
1717
s := server.NewMCPServer(
1818
"Slack MCP Server",
19-
"1.0.0",
19+
"1.1.11",
2020
server.WithLogging(),
2121
server.WithRecovery(),
2222
)
@@ -38,6 +38,25 @@ func NewMCPServer(provider *provider.ApiProvider) *MCPServer {
3838
),
3939
), conversationsHandler.ConversationsHistoryHandler)
4040

41+
s.AddTool(mcp.NewTool("conversations_replies",
42+
mcp.WithDescription("Get a thread of messages posted to a conversation by channelID and thread_ts, the last row/column in the response is used as 'cursor' parameter for pagination if not empty"),
43+
mcp.WithString("channel_id",
44+
mcp.Required(),
45+
mcp.Description("ID of the channel in format Cxxxxxxxxxx"),
46+
),
47+
mcp.WithString("thread_ts",
48+
mcp.Required(),
49+
mcp.Description("Unique identifier of either a thread’s parent message or a message in the thread. ts must be the timestamp in format 1234567890.123456 of an existing message with 0 or more replies."),
50+
),
51+
mcp.WithString("cursor",
52+
mcp.Description("Cursor for pagination. Use the value of the last row and column in the response as next_cursor field returned from the previous request."),
53+
),
54+
mcp.WithString("limit",
55+
mcp.DefaultString("1d"),
56+
mcp.Description("Limit of messages to fetch in format of maximum ranges of time (e.g. 1d - 1 day, 30d - 30 days, 90d - 90 days which is a default limit for free tier history) or number of messages (e.g. 50). Must be empty when 'cursor' is provided."),
57+
),
58+
), conversationsHandler.ConversationsRepliesHandler)
59+
4160
channelsHandler := handler.NewChannelsHandler(provider)
4261

4362
s.AddTool(mcp.NewTool("channels_list",

0 commit comments

Comments
 (0)