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
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand Down
41 changes: 38 additions & 3 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<any> {
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 },
Expand Down Expand Up @@ -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) }],
};
Expand Down
263 changes: 263 additions & 0 deletions tests/slack-mcp-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down