From 49142b80bb324cb9809dceb4d0074eb39bdd696a Mon Sep 17 00:00:00 2001 From: Konstantin Pletenev Date: Wed, 23 Jul 2025 14:08:36 +0100 Subject: [PATCH] feat: enhance slack_get_channel_history with additional Slack API parameters - Add support for cursor pagination - Add include_all_metadata parameter for message metadata - Add inclusive parameter for timestamp boundary behavior - Add latest parameter for end-time filtering - Add oldest parameter for start-time filtering - Update TypeScript interfaces and Zod schema validation - Add comprehensive test suite (8 new tests) covering all new parameters - Update README documentation with parameter descriptions and examples - Maintain backward compatibility with existing code All tests passing (28/28). Enhanced functionality enables time-period based chat history retrieval and improved pagination control. --- README.md | 7 +- index.ts | 41 ++++- tests/slack-mcp-server.test.ts | 263 +++++++++++++++++++++++++++++++++ 3 files changed, 307 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 281105c..9c64825 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,16 @@ A Model Context Protocol (MCP) server for interacting with Slack workspaces. Thi - Returns: Reaction confirmation 5. **slack_get_channel_history** - - Get recent messages from a channel + - Get messages from a channel with optional time range and pagination support - Required inputs: - `channel_id` (string): The channel ID - Optional inputs: - `limit` (number, default: 10): Number of messages to retrieve + - `cursor` (string): Pagination cursor for next page of results + - `include_all_metadata` (boolean): Return all metadata associated with messages + - `inclusive` (boolean): Include messages at the exact timestamps specified by oldest and latest + - `latest` (string): End of time range of messages to include (timestamp, e.g., '1234567890.123456') + - `oldest` (string): Start of time range of messages to include (timestamp, e.g., '1234567890.123456') - Returns: List of messages with their content and metadata 6. **slack_get_thread_replies** diff --git a/index.ts b/index.ts index 4687151..70aa1df 100644 --- a/index.ts +++ b/index.ts @@ -34,6 +34,11 @@ interface AddReactionArgs { interface GetChannelHistoryArgs { channel_id: string; limit?: number; + cursor?: string; + include_all_metadata?: boolean; + inclusive?: boolean; + latest?: string; + oldest?: string; } interface GetThreadRepliesArgs { @@ -160,12 +165,37 @@ export class SlackClient { async getChannelHistory( channel_id: string, limit: number = 10, + cursor?: string, + include_all_metadata?: boolean, + inclusive?: boolean, + latest?: string, + oldest?: string, ): Promise { const params = new URLSearchParams({ channel: channel_id, limit: limit.toString(), }); + if (cursor) { + params.append("cursor", cursor); + } + + if (include_all_metadata !== undefined) { + params.append("include_all_metadata", include_all_metadata.toString()); + } + + if (inclusive !== undefined) { + params.append("inclusive", inclusive.toString()); + } + + if (latest) { + params.append("latest", latest); + } + + if (oldest) { + params.append("oldest", oldest); + } + const response = await fetch( `https://slack.com/api/conversations.history?${params}`, { headers: this.botHeaders }, @@ -305,14 +335,19 @@ export function createSlackServer(slackClient: SlackClient): McpServer { "slack_get_channel_history", { title: "Get Slack Channel History", - description: "Get recent messages from a channel", + description: "Get messages from a channel with optional time range and pagination support", inputSchema: { channel_id: z.string().describe("The ID of the channel"), limit: z.number().optional().default(10).describe("Number of messages to retrieve (default 10)"), + cursor: z.string().optional().describe("Pagination cursor for next page of results"), + include_all_metadata: z.boolean().optional().describe("Return all metadata associated with messages"), + inclusive: z.boolean().optional().describe("Include messages at the exact timestamps specified by oldest and latest"), + latest: z.string().optional().describe("End of time range of messages to include (timestamp, e.g., '1234567890.123456')"), + oldest: z.string().optional().describe("Start of time range of messages to include (timestamp, e.g., '1234567890.123456')"), }, }, - async ({ channel_id, limit }) => { - const response = await slackClient.getChannelHistory(channel_id, limit); + async ({ channel_id, limit, cursor, include_all_metadata, inclusive, latest, oldest }) => { + const response = await slackClient.getChannelHistory(channel_id, limit, cursor, include_all_metadata, inclusive, latest, oldest); 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..fd0548f 100644 --- a/tests/slack-mcp-server.test.ts +++ b/tests/slack-mcp-server.test.ts @@ -264,6 +264,269 @@ describe('SlackClient', () => { ); }); + test('getChannelHistory with cursor parameter', async () => { + const mockResponse = { + ok: true, + messages: [], + response_metadata: { next_cursor: 'next_cursor_value' }, + }; + + mockFetch.mockResolvedValueOnce({ + json: () => Promise.resolve(mockResponse), + }); + + const result = await slackClient.getChannelHistory('C123456', 10, 'test_cursor'); + + expect(result).toEqual(mockResponse); + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining('cursor=test_cursor'), + expect.objectContaining({ + headers: { + Authorization: 'Bearer xoxb-test-token', + 'Content-Type': 'application/json', + }, + }) + ); + }); + + test('getChannelHistory with include_all_metadata parameter', async () => { + const mockResponse = { + ok: true, + messages: [ + { + type: 'message', + user: 'U123456', + text: 'Hello', + ts: '1234567890.123456', + metadata: { event_type: 'app_mention' }, + }, + ], + }; + + mockFetch.mockResolvedValueOnce({ + json: () => Promise.resolve(mockResponse), + }); + + const result = await slackClient.getChannelHistory('C123456', 10, undefined, true); + + expect(result).toEqual(mockResponse); + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining('include_all_metadata=true'), + expect.objectContaining({ + headers: { + Authorization: 'Bearer xoxb-test-token', + 'Content-Type': 'application/json', + }, + }) + ); + }); + + test('getChannelHistory with inclusive parameter', async () => { + const mockResponse = { + ok: true, + messages: [ + { + type: 'message', + user: 'U123456', + text: 'Hello', + ts: '1234567890.123456', + }, + ], + }; + + mockFetch.mockResolvedValueOnce({ + json: () => Promise.resolve(mockResponse), + }); + + const result = await slackClient.getChannelHistory('C123456', 10, undefined, undefined, false); + + expect(result).toEqual(mockResponse); + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining('inclusive=false'), + expect.objectContaining({ + headers: { + Authorization: 'Bearer xoxb-test-token', + 'Content-Type': 'application/json', + }, + }) + ); + }); + + test('getChannelHistory with latest parameter', async () => { + const mockResponse = { + ok: true, + messages: [ + { + type: 'message', + user: 'U123456', + text: 'Hello', + ts: '1234567890.123456', + }, + ], + }; + + mockFetch.mockResolvedValueOnce({ + json: () => Promise.resolve(mockResponse), + }); + + const result = await slackClient.getChannelHistory('C123456', 10, undefined, undefined, undefined, '1640995200.000000'); + + expect(result).toEqual(mockResponse); + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining('latest=1640995200.000000'), + expect.objectContaining({ + headers: { + Authorization: 'Bearer xoxb-test-token', + 'Content-Type': 'application/json', + }, + }) + ); + }); + + test('getChannelHistory with oldest parameter', async () => { + const mockResponse = { + ok: true, + messages: [ + { + type: 'message', + user: 'U123456', + text: 'Hello', + ts: '1234567890.123456', + }, + ], + }; + + mockFetch.mockResolvedValueOnce({ + json: () => Promise.resolve(mockResponse), + }); + + const result = await slackClient.getChannelHistory('C123456', 10, undefined, undefined, undefined, undefined, '1640908800.000000'); + + expect(result).toEqual(mockResponse); + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining('oldest=1640908800.000000'), + expect.objectContaining({ + headers: { + Authorization: 'Bearer xoxb-test-token', + 'Content-Type': 'application/json', + }, + }) + ); + }); + + test('getChannelHistory with time range (oldest and latest)', async () => { + const mockResponse = { + ok: true, + messages: [ + { + type: 'message', + user: 'U123456', + text: 'Hello', + ts: '1234567890.123456', + }, + ], + }; + + mockFetch.mockResolvedValueOnce({ + json: () => Promise.resolve(mockResponse), + }); + + const result = await slackClient.getChannelHistory( + 'C123456', + 50, + undefined, + undefined, + true, + '1640995200.000000', + '1640908800.000000' + ); + + expect(result).toEqual(mockResponse); + const fetchCall = mockFetch.mock.calls[mockFetch.mock.calls.length - 1]; + const url = fetchCall[0]; + + expect(url).toContain('oldest=1640908800.000000'); + expect(url).toContain('latest=1640995200.000000'); + expect(url).toContain('inclusive=true'); + expect(url).toContain('limit=50'); + }); + + test('getChannelHistory with all parameters', async () => { + const mockResponse = { + ok: true, + messages: [ + { + type: 'message', + user: 'U123456', + text: 'Hello', + ts: '1234567890.123456', + metadata: { event_type: 'app_mention' }, + }, + ], + response_metadata: { next_cursor: 'next_cursor_value' }, + }; + + mockFetch.mockResolvedValueOnce({ + json: () => Promise.resolve(mockResponse), + }); + + const result = await slackClient.getChannelHistory( + 'C123456', + 25, + 'test_cursor', + true, + false, + '1640995200.000000', + '1640908800.000000' + ); + + expect(result).toEqual(mockResponse); + const fetchCall = mockFetch.mock.calls[mockFetch.mock.calls.length - 1]; + const url = fetchCall[0]; + + expect(url).toContain('channel=C123456'); + expect(url).toContain('limit=25'); + expect(url).toContain('cursor=test_cursor'); + expect(url).toContain('include_all_metadata=true'); + expect(url).toContain('inclusive=false'); + expect(url).toContain('latest=1640995200.000000'); + expect(url).toContain('oldest=1640908800.000000'); + }); + + test('getChannelHistory with undefined optional parameters', async () => { + const mockResponse = { + ok: true, + messages: [], + }; + + mockFetch.mockResolvedValueOnce({ + json: () => Promise.resolve(mockResponse), + }); + + const result = await slackClient.getChannelHistory( + 'C123456', + 10, + undefined, + undefined, + undefined, + undefined, + undefined + ); + + expect(result).toEqual(mockResponse); + const fetchCall = mockFetch.mock.calls[mockFetch.mock.calls.length - 1]; + const url = fetchCall[0]; + + // Should only contain required parameters + expect(url).toContain('channel=C123456'); + expect(url).toContain('limit=10'); + expect(url).not.toContain('cursor='); + expect(url).not.toContain('include_all_metadata='); + expect(url).not.toContain('inclusive='); + expect(url).not.toContain('latest='); + expect(url).not.toContain('oldest='); + }); + test('getThreadReplies successful response', async () => { const mockResponse = { ok: true,