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
24 changes: 21 additions & 3 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,20 @@ export class SlackClient {
return response.json();
}

async getThreadReplies(channel_id: string, thread_ts: string): Promise<any> {
async getThreadReplies(
channel_id: string,
thread_ts: string,
oldest?: string,
limit?: number,
cursor?: string,
): Promise<any> {
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}`,
Expand Down Expand Up @@ -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) }],
};
Expand Down
37 changes: 37 additions & 0 deletions tests/slack-mcp-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down