diff --git a/index.ts b/index.ts index 4687151..0fd9f90 100644 --- a/index.ts +++ b/index.ts @@ -174,11 +174,20 @@ export class SlackClient { return response.json(); } - async getThreadReplies(channel_id: string, thread_ts: string): Promise { + async getThreadReplies( + channel_id: string, + thread_ts: string, + oldest?: string, + limit?: number, + cursor?: string, + ): Promise { const params = new URLSearchParams({ channel: channel_id, ts: thread_ts, }); + if (oldest) params.set("oldest", oldest); + if (limit) params.set("limit", String(limit)); + if (cursor) params.set("cursor", cursor); const response = await fetch( `https://slack.com/api/conversations.replies?${params}`, @@ -327,10 +336,19 @@ export function createSlackServer(slackClient: SlackClient): McpServer { inputSchema: { channel_id: z.string().describe("The ID of the channel containing the thread"), thread_ts: z.string().describe("The timestamp of the parent message in the format '1234567890.123456'. Timestamps in the format without the period can be converted by adding the period such that 6 numbers come after it."), + oldest: z.string().optional().describe("Only return replies after this Unix timestamp. Use to fetch only new replies since a previous call."), + limit: z.number().optional().describe("Maximum number of replies to return per call"), + cursor: z.string().optional().describe("Pagination cursor from a previous response's next_cursor"), }, }, - async ({ channel_id, thread_ts }) => { - const response = await slackClient.getThreadReplies(channel_id, thread_ts); + async ({ channel_id, thread_ts, oldest, limit, cursor }) => { + const response = await slackClient.getThreadReplies( + channel_id, + thread_ts, + oldest, + limit, + cursor, + ); return { content: [{ type: "text", text: JSON.stringify(response) }], }; diff --git a/tests/slack-mcp-server.test.ts b/tests/slack-mcp-server.test.ts index 43d448b..9cc0fae 100644 --- a/tests/slack-mcp-server.test.ts +++ b/tests/slack-mcp-server.test.ts @@ -302,6 +302,43 @@ describe('SlackClient', () => { ); }); + test('getThreadReplies with oldest only', async () => { + mockFetch.mockResolvedValueOnce({ + json: () => Promise.resolve({ ok: true, messages: [] }), + }); + + await slackClient.getThreadReplies('C123456', '1234567890.123456', '1234567891.000000'); + + const calledUrl: string = (mockFetch as any).mock.calls[0][0]; + expect(calledUrl).toContain('oldest=1234567891.000000'); + expect(calledUrl).not.toContain('limit='); + expect(calledUrl).not.toContain('cursor='); + }); + + test('getThreadReplies with limit and cursor for paginated continuation', async () => { + const mockResponse = { + ok: true, + messages: [ + { type: 'message', user: 'U789012', text: 'Reply', ts: '1234567892.000000' }, + ], + response_metadata: { next_cursor: 'page2' }, + }; + + mockFetch.mockResolvedValueOnce({ + json: () => Promise.resolve(mockResponse), + }); + + const result = await slackClient.getThreadReplies( + 'C123456', '1234567890.123456', undefined, 25, 'abc123' + ); + + const calledUrl: string = (mockFetch as any).mock.calls[0][0]; + expect(calledUrl).toContain('limit=25'); + expect(calledUrl).toContain('cursor=abc123'); + expect(calledUrl).not.toContain('oldest='); + expect(result.response_metadata.next_cursor).toBe('page2'); + }); + test('getUsers successful response', async () => { const mockResponse = { ok: true,