slack-workspace-mcp-server 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -0
- package/build/index.d.ts +2 -0
- package/build/index.integration-with-mock.d.ts +2 -0
- package/build/index.integration-with-mock.js +34 -0
- package/build/index.js +76 -0
- package/package.json +47 -0
- package/shared/index.d.ts +4 -0
- package/shared/index.js +7 -0
- package/shared/logging.d.ts +10 -0
- package/shared/logging.js +21 -0
- package/shared/server.d.ts +134 -0
- package/shared/server.js +68 -0
- package/shared/slack-client/lib/add-reaction.d.ts +5 -0
- package/shared/slack-client/lib/add-reaction.js +30 -0
- package/shared/slack-client/lib/get-channel.d.ts +5 -0
- package/shared/slack-client/lib/get-channel.js +24 -0
- package/shared/slack-client/lib/get-channels.d.ts +10 -0
- package/shared/slack-client/lib/get-channels.js +37 -0
- package/shared/slack-client/lib/get-messages.d.ts +16 -0
- package/shared/slack-client/lib/get-messages.js +38 -0
- package/shared/slack-client/lib/get-thread.d.ts +16 -0
- package/shared/slack-client/lib/get-thread.js +39 -0
- package/shared/slack-client/lib/post-message.d.ts +10 -0
- package/shared/slack-client/lib/post-message.js +44 -0
- package/shared/slack-client/lib/update-message.d.ts +5 -0
- package/shared/slack-client/lib/update-message.js +32 -0
- package/shared/slack-client/slack-client.integration-mock.d.ts +16 -0
- package/shared/slack-client/slack-client.integration-mock.js +106 -0
- package/shared/tools/get-channel.d.ts +55 -0
- package/shared/tools/get-channel.js +136 -0
- package/shared/tools/get-channels.d.ts +46 -0
- package/shared/tools/get-channels.js +107 -0
- package/shared/tools/get-thread.d.ts +54 -0
- package/shared/tools/get-thread.js +130 -0
- package/shared/tools/post-message.d.ts +44 -0
- package/shared/tools/post-message.js +81 -0
- package/shared/tools/react-to-message.d.ts +51 -0
- package/shared/tools/react-to-message.js +90 -0
- package/shared/tools/reply-to-thread.d.ts +59 -0
- package/shared/tools/reply-to-thread.js +97 -0
- package/shared/tools/update-message.d.ts +51 -0
- package/shared/tools/update-message.js +87 -0
- package/shared/tools.d.ts +18 -0
- package/shared/tools.js +78 -0
- package/shared/types.d.ts +112 -0
- package/shared/types.js +5 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetches message history from a channel
|
|
3
|
+
* Returns messages in reverse chronological order (newest first)
|
|
4
|
+
*/
|
|
5
|
+
export async function getMessages(baseUrl, headers, channelId, options) {
|
|
6
|
+
const params = new URLSearchParams({
|
|
7
|
+
channel: channelId,
|
|
8
|
+
limit: (options?.limit ?? 20).toString(),
|
|
9
|
+
});
|
|
10
|
+
if (options?.cursor) {
|
|
11
|
+
params.set('cursor', options.cursor);
|
|
12
|
+
}
|
|
13
|
+
if (options?.oldest) {
|
|
14
|
+
params.set('oldest', options.oldest);
|
|
15
|
+
}
|
|
16
|
+
if (options?.latest) {
|
|
17
|
+
params.set('latest', options.latest);
|
|
18
|
+
}
|
|
19
|
+
if (options?.inclusive !== undefined) {
|
|
20
|
+
params.set('inclusive', options.inclusive.toString());
|
|
21
|
+
}
|
|
22
|
+
const response = await fetch(`${baseUrl}/conversations.history?${params}`, {
|
|
23
|
+
method: 'GET',
|
|
24
|
+
headers,
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error(`Failed to fetch messages: ${response.status} ${response.statusText}`);
|
|
28
|
+
}
|
|
29
|
+
const data = (await response.json());
|
|
30
|
+
if (!data.ok) {
|
|
31
|
+
throw new Error(`Slack API error: ${data.error}`);
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
messages: data.messages ?? [],
|
|
35
|
+
hasMore: data.has_more ?? false,
|
|
36
|
+
nextCursor: data.response_metadata?.next_cursor,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Message } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Fetches all replies in a thread
|
|
4
|
+
* The first message is the parent message, followed by replies
|
|
5
|
+
*/
|
|
6
|
+
export declare function getThread(baseUrl: string, headers: Record<string, string>, channelId: string, threadTs: string, options?: {
|
|
7
|
+
limit?: number;
|
|
8
|
+
cursor?: string;
|
|
9
|
+
oldest?: string;
|
|
10
|
+
latest?: string;
|
|
11
|
+
inclusive?: boolean;
|
|
12
|
+
}): Promise<{
|
|
13
|
+
messages: Message[];
|
|
14
|
+
hasMore: boolean;
|
|
15
|
+
nextCursor?: string;
|
|
16
|
+
}>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetches all replies in a thread
|
|
3
|
+
* The first message is the parent message, followed by replies
|
|
4
|
+
*/
|
|
5
|
+
export async function getThread(baseUrl, headers, channelId, threadTs, options) {
|
|
6
|
+
const params = new URLSearchParams({
|
|
7
|
+
channel: channelId,
|
|
8
|
+
ts: threadTs,
|
|
9
|
+
limit: (options?.limit ?? 100).toString(),
|
|
10
|
+
});
|
|
11
|
+
if (options?.cursor) {
|
|
12
|
+
params.set('cursor', options.cursor);
|
|
13
|
+
}
|
|
14
|
+
if (options?.oldest) {
|
|
15
|
+
params.set('oldest', options.oldest);
|
|
16
|
+
}
|
|
17
|
+
if (options?.latest) {
|
|
18
|
+
params.set('latest', options.latest);
|
|
19
|
+
}
|
|
20
|
+
if (options?.inclusive !== undefined) {
|
|
21
|
+
params.set('inclusive', options.inclusive.toString());
|
|
22
|
+
}
|
|
23
|
+
const response = await fetch(`${baseUrl}/conversations.replies?${params}`, {
|
|
24
|
+
method: 'GET',
|
|
25
|
+
headers,
|
|
26
|
+
});
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
throw new Error(`Failed to fetch thread: ${response.status} ${response.statusText}`);
|
|
29
|
+
}
|
|
30
|
+
const data = (await response.json());
|
|
31
|
+
if (!data.ok) {
|
|
32
|
+
throw new Error(`Slack API error: ${data.error}`);
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
messages: data.messages ?? [],
|
|
36
|
+
hasMore: data.has_more ?? false,
|
|
37
|
+
nextCursor: data.response_metadata?.next_cursor,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Message } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Posts a new message to a channel
|
|
4
|
+
*/
|
|
5
|
+
export declare function postMessage(baseUrl: string, headers: Record<string, string>, channelId: string, text: string, options?: {
|
|
6
|
+
threadTs?: string;
|
|
7
|
+
replyBroadcast?: boolean;
|
|
8
|
+
unfurlLinks?: boolean;
|
|
9
|
+
unfurlMedia?: boolean;
|
|
10
|
+
}): Promise<Message>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Posts a new message to a channel
|
|
3
|
+
*/
|
|
4
|
+
export async function postMessage(baseUrl, headers, channelId, text, options) {
|
|
5
|
+
const body = {
|
|
6
|
+
channel: channelId,
|
|
7
|
+
text,
|
|
8
|
+
};
|
|
9
|
+
if (options?.threadTs) {
|
|
10
|
+
body.thread_ts = options.threadTs;
|
|
11
|
+
}
|
|
12
|
+
if (options?.replyBroadcast !== undefined) {
|
|
13
|
+
body.reply_broadcast = options.replyBroadcast;
|
|
14
|
+
}
|
|
15
|
+
if (options?.unfurlLinks !== undefined) {
|
|
16
|
+
body.unfurl_links = options.unfurlLinks;
|
|
17
|
+
}
|
|
18
|
+
if (options?.unfurlMedia !== undefined) {
|
|
19
|
+
body.unfurl_media = options.unfurlMedia;
|
|
20
|
+
}
|
|
21
|
+
const response = await fetch(`${baseUrl}/chat.postMessage`, {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: {
|
|
24
|
+
...headers,
|
|
25
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify(body),
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw new Error(`Failed to post message: ${response.status} ${response.statusText}`);
|
|
31
|
+
}
|
|
32
|
+
const data = (await response.json());
|
|
33
|
+
if (!data.ok) {
|
|
34
|
+
throw new Error(`Slack API error: ${data.error}`);
|
|
35
|
+
}
|
|
36
|
+
if (!data.message) {
|
|
37
|
+
throw new Error('Message not found in response');
|
|
38
|
+
}
|
|
39
|
+
// Ensure ts is set on the message
|
|
40
|
+
return {
|
|
41
|
+
...data.message,
|
|
42
|
+
ts: data.ts ?? data.message.ts,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Updates an existing message
|
|
3
|
+
*/
|
|
4
|
+
export async function updateMessage(baseUrl, headers, channelId, ts, text) {
|
|
5
|
+
const body = {
|
|
6
|
+
channel: channelId,
|
|
7
|
+
ts,
|
|
8
|
+
text,
|
|
9
|
+
};
|
|
10
|
+
const response = await fetch(`${baseUrl}/chat.update`, {
|
|
11
|
+
method: 'POST',
|
|
12
|
+
headers: {
|
|
13
|
+
...headers,
|
|
14
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
15
|
+
},
|
|
16
|
+
body: JSON.stringify(body),
|
|
17
|
+
});
|
|
18
|
+
if (!response.ok) {
|
|
19
|
+
throw new Error(`Failed to update message: ${response.status} ${response.statusText}`);
|
|
20
|
+
}
|
|
21
|
+
const data = (await response.json());
|
|
22
|
+
if (!data.ok) {
|
|
23
|
+
throw new Error(`Slack API error: ${data.error}`);
|
|
24
|
+
}
|
|
25
|
+
// Return the updated message
|
|
26
|
+
return {
|
|
27
|
+
type: 'message',
|
|
28
|
+
ts: data.ts ?? ts,
|
|
29
|
+
text: data.text ?? text,
|
|
30
|
+
...data.message,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ISlackClient } from '../server.js';
|
|
2
|
+
import type { Channel, Message } from '../types.js';
|
|
3
|
+
interface MockData {
|
|
4
|
+
channels?: Channel[];
|
|
5
|
+
messages?: Record<string, Message[]>;
|
|
6
|
+
threads?: Record<string, Message[]>;
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Creates a mock implementation of ISlackClient for integration tests.
|
|
11
|
+
* This mocks the EXTERNAL Slack API, NOT the MCP client.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createIntegrationMockSlackClient(mockData?: MockData): ISlackClient & {
|
|
14
|
+
mockData: MockData;
|
|
15
|
+
};
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a mock implementation of ISlackClient for integration tests.
|
|
3
|
+
* This mocks the EXTERNAL Slack API, NOT the MCP client.
|
|
4
|
+
*/
|
|
5
|
+
export function createIntegrationMockSlackClient(mockData = {}) {
|
|
6
|
+
// Track posted messages for testing
|
|
7
|
+
const postedMessages = [];
|
|
8
|
+
const client = {
|
|
9
|
+
mockData,
|
|
10
|
+
async getChannels() {
|
|
11
|
+
return (mockData.channels ?? [
|
|
12
|
+
{
|
|
13
|
+
id: 'C123456789',
|
|
14
|
+
name: 'general',
|
|
15
|
+
is_channel: true,
|
|
16
|
+
is_group: false,
|
|
17
|
+
is_im: false,
|
|
18
|
+
is_mpim: false,
|
|
19
|
+
is_private: false,
|
|
20
|
+
is_archived: false,
|
|
21
|
+
is_general: true,
|
|
22
|
+
is_member: true,
|
|
23
|
+
num_members: 10,
|
|
24
|
+
created: 1234567890,
|
|
25
|
+
},
|
|
26
|
+
]);
|
|
27
|
+
},
|
|
28
|
+
async getChannel(channelId) {
|
|
29
|
+
const channel = mockData.channels?.find((c) => c.id === channelId);
|
|
30
|
+
if (channel) {
|
|
31
|
+
return channel;
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
id: channelId,
|
|
35
|
+
name: 'mock-channel',
|
|
36
|
+
is_channel: true,
|
|
37
|
+
is_group: false,
|
|
38
|
+
is_im: false,
|
|
39
|
+
is_mpim: false,
|
|
40
|
+
is_private: false,
|
|
41
|
+
is_archived: false,
|
|
42
|
+
is_general: false,
|
|
43
|
+
is_member: true,
|
|
44
|
+
created: 1234567890,
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
async getMessages(channelId, options) {
|
|
48
|
+
const messages = mockData.messages?.[channelId] ?? [
|
|
49
|
+
{
|
|
50
|
+
type: 'message',
|
|
51
|
+
user: 'U123456789',
|
|
52
|
+
text: 'Mock message',
|
|
53
|
+
ts: '1234567890.123456',
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
const limit = options?.limit ?? 20;
|
|
57
|
+
return {
|
|
58
|
+
messages: messages.slice(0, limit),
|
|
59
|
+
hasMore: messages.length > limit,
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
async getThread(channelId, threadTs, options) {
|
|
63
|
+
const key = `${channelId}:${threadTs}`;
|
|
64
|
+
const messages = mockData.threads?.[key] ?? [
|
|
65
|
+
{
|
|
66
|
+
type: 'message',
|
|
67
|
+
user: 'U123456789',
|
|
68
|
+
text: 'Mock parent message',
|
|
69
|
+
ts: threadTs,
|
|
70
|
+
thread_ts: threadTs,
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
const limit = options?.limit ?? 50;
|
|
74
|
+
return {
|
|
75
|
+
messages: messages.slice(0, limit),
|
|
76
|
+
hasMore: messages.length > limit,
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
async postMessage(channelId, text, options) {
|
|
80
|
+
const ts = `${Date.now() / 1000}.123456`;
|
|
81
|
+
const message = {
|
|
82
|
+
type: 'message',
|
|
83
|
+
text,
|
|
84
|
+
ts,
|
|
85
|
+
thread_ts: options?.threadTs,
|
|
86
|
+
};
|
|
87
|
+
postedMessages.push(message);
|
|
88
|
+
return message;
|
|
89
|
+
},
|
|
90
|
+
async updateMessage(channelId, ts, text) {
|
|
91
|
+
return {
|
|
92
|
+
type: 'message',
|
|
93
|
+
text,
|
|
94
|
+
ts,
|
|
95
|
+
edited: {
|
|
96
|
+
user: 'U123456789',
|
|
97
|
+
ts: `${Date.now() / 1000}`,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
async addReaction() {
|
|
102
|
+
// No-op for mock
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
return client;
|
|
106
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { ClientFactory } from '../server.js';
|
|
4
|
+
export declare const GetChannelSchema: z.ZodObject<{
|
|
5
|
+
channel_id: z.ZodString;
|
|
6
|
+
include_messages: z.ZodDefault<z.ZodBoolean>;
|
|
7
|
+
message_limit: z.ZodDefault<z.ZodNumber>;
|
|
8
|
+
}, "strip", z.ZodTypeAny, {
|
|
9
|
+
channel_id: string;
|
|
10
|
+
include_messages: boolean;
|
|
11
|
+
message_limit: number;
|
|
12
|
+
}, {
|
|
13
|
+
channel_id: string;
|
|
14
|
+
include_messages?: boolean | undefined;
|
|
15
|
+
message_limit?: number | undefined;
|
|
16
|
+
}>;
|
|
17
|
+
export declare function getChannelTool(server: Server, clientFactory: ClientFactory): {
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
inputSchema: {
|
|
21
|
+
type: "object";
|
|
22
|
+
properties: {
|
|
23
|
+
channel_id: {
|
|
24
|
+
type: string;
|
|
25
|
+
description: string;
|
|
26
|
+
};
|
|
27
|
+
include_messages: {
|
|
28
|
+
type: string;
|
|
29
|
+
default: boolean;
|
|
30
|
+
description: string;
|
|
31
|
+
};
|
|
32
|
+
message_limit: {
|
|
33
|
+
type: string;
|
|
34
|
+
default: number;
|
|
35
|
+
minimum: number;
|
|
36
|
+
maximum: number;
|
|
37
|
+
description: string;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
required: string[];
|
|
41
|
+
};
|
|
42
|
+
handler: (args: unknown) => Promise<{
|
|
43
|
+
content: {
|
|
44
|
+
type: string;
|
|
45
|
+
text: string;
|
|
46
|
+
}[];
|
|
47
|
+
isError?: undefined;
|
|
48
|
+
} | {
|
|
49
|
+
content: {
|
|
50
|
+
type: string;
|
|
51
|
+
text: string;
|
|
52
|
+
}[];
|
|
53
|
+
isError: boolean;
|
|
54
|
+
}>;
|
|
55
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const PARAM_DESCRIPTIONS = {
|
|
3
|
+
channel_id: 'The unique identifier of the channel (e.g., "C1234567890"). ' +
|
|
4
|
+
'Get channel IDs using the slack_get_channels tool.',
|
|
5
|
+
include_messages: 'Whether to include recent messages from the channel. Default: true. ' +
|
|
6
|
+
'Set to false to only get channel metadata.',
|
|
7
|
+
message_limit: 'Maximum number of messages to return when include_messages is true. ' +
|
|
8
|
+
'Default: 20. Maximum: 100.',
|
|
9
|
+
};
|
|
10
|
+
export const GetChannelSchema = z.object({
|
|
11
|
+
channel_id: z.string().min(1).describe(PARAM_DESCRIPTIONS.channel_id),
|
|
12
|
+
include_messages: z.boolean().default(true).describe(PARAM_DESCRIPTIONS.include_messages),
|
|
13
|
+
message_limit: z.number().min(1).max(100).default(20).describe(PARAM_DESCRIPTIONS.message_limit),
|
|
14
|
+
});
|
|
15
|
+
const TOOL_DESCRIPTION = `Get detailed information about a Slack channel, optionally including recent messages.
|
|
16
|
+
|
|
17
|
+
Returns channel metadata (name, topic, purpose, member count) and optionally the most recent messages. This is equivalent to opening a channel in Slack.
|
|
18
|
+
|
|
19
|
+
**Returns:**
|
|
20
|
+
- Channel details: name, topic, purpose, member count, creation date
|
|
21
|
+
- Recent messages (if include_messages is true): sender, content, timestamp, reactions, thread info
|
|
22
|
+
|
|
23
|
+
**Use cases:**
|
|
24
|
+
- Get context about a channel before posting
|
|
25
|
+
- Read recent messages to understand current discussions
|
|
26
|
+
- Check if a channel is active or archived
|
|
27
|
+
- Find threads that may need responses
|
|
28
|
+
|
|
29
|
+
**Note:** Messages are returned newest first. Thread parent messages include reply counts.`;
|
|
30
|
+
export function getChannelTool(server, clientFactory) {
|
|
31
|
+
return {
|
|
32
|
+
name: 'slack_get_channel',
|
|
33
|
+
description: TOOL_DESCRIPTION,
|
|
34
|
+
inputSchema: {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
channel_id: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
description: PARAM_DESCRIPTIONS.channel_id,
|
|
40
|
+
},
|
|
41
|
+
include_messages: {
|
|
42
|
+
type: 'boolean',
|
|
43
|
+
default: true,
|
|
44
|
+
description: PARAM_DESCRIPTIONS.include_messages,
|
|
45
|
+
},
|
|
46
|
+
message_limit: {
|
|
47
|
+
type: 'number',
|
|
48
|
+
default: 20,
|
|
49
|
+
minimum: 1,
|
|
50
|
+
maximum: 100,
|
|
51
|
+
description: PARAM_DESCRIPTIONS.message_limit,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
required: ['channel_id'],
|
|
55
|
+
},
|
|
56
|
+
handler: async (args) => {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = GetChannelSchema.parse(args);
|
|
59
|
+
const client = clientFactory();
|
|
60
|
+
// Get channel info
|
|
61
|
+
const channel = await client.getChannel(parsed.channel_id);
|
|
62
|
+
// Build channel info section
|
|
63
|
+
const flags = [];
|
|
64
|
+
if (channel.is_private)
|
|
65
|
+
flags.push('private');
|
|
66
|
+
if (channel.is_archived)
|
|
67
|
+
flags.push('archived');
|
|
68
|
+
if (channel.is_general)
|
|
69
|
+
flags.push('general');
|
|
70
|
+
let output = `# Channel: #${channel.name}\n`;
|
|
71
|
+
if (flags.length > 0) {
|
|
72
|
+
output += `Status: ${flags.join(', ')}\n`;
|
|
73
|
+
}
|
|
74
|
+
output += `ID: ${channel.id}\n`;
|
|
75
|
+
if (channel.topic?.value) {
|
|
76
|
+
output += `Topic: ${channel.topic.value}\n`;
|
|
77
|
+
}
|
|
78
|
+
if (channel.purpose?.value) {
|
|
79
|
+
output += `Purpose: ${channel.purpose.value}\n`;
|
|
80
|
+
}
|
|
81
|
+
if (channel.num_members !== undefined) {
|
|
82
|
+
output += `Members: ${channel.num_members}\n`;
|
|
83
|
+
}
|
|
84
|
+
output += `Created: ${new Date(channel.created * 1000).toISOString()}\n`;
|
|
85
|
+
// Get messages if requested
|
|
86
|
+
if (parsed.include_messages) {
|
|
87
|
+
const { messages, hasMore } = await client.getMessages(parsed.channel_id, {
|
|
88
|
+
limit: parsed.message_limit,
|
|
89
|
+
});
|
|
90
|
+
if (messages.length > 0) {
|
|
91
|
+
output += `\n## Recent Messages (${messages.length}${hasMore ? '+' : ''}):\n\n`;
|
|
92
|
+
for (const msg of messages) {
|
|
93
|
+
const time = new Date(parseFloat(msg.ts) * 1000).toISOString();
|
|
94
|
+
const sender = msg.user || msg.bot_id || 'unknown';
|
|
95
|
+
const threadInfo = msg.reply_count ? ` [${msg.reply_count} replies]` : '';
|
|
96
|
+
const reactions = msg.reactions
|
|
97
|
+
? ` | Reactions: ${msg.reactions.map((r) => `:${r.name}: ${r.count}`).join(' ')}`
|
|
98
|
+
: '';
|
|
99
|
+
output += `**${sender}** (${time})${threadInfo}${reactions}\n`;
|
|
100
|
+
output += `${msg.text}\n`;
|
|
101
|
+
if (msg.thread_ts && msg.thread_ts !== msg.ts) {
|
|
102
|
+
output += `↳ Reply to thread: ${msg.thread_ts}\n`;
|
|
103
|
+
}
|
|
104
|
+
output += `ts: ${msg.ts}\n\n`;
|
|
105
|
+
}
|
|
106
|
+
if (hasMore) {
|
|
107
|
+
output += `_More messages available. Increase message_limit to see more._\n`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
output += '\n_No messages in this channel yet._\n';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
content: [
|
|
116
|
+
{
|
|
117
|
+
type: 'text',
|
|
118
|
+
text: output,
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
return {
|
|
125
|
+
content: [
|
|
126
|
+
{
|
|
127
|
+
type: 'text',
|
|
128
|
+
text: `Error fetching channel: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
isError: true,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { ClientFactory } from '../server.js';
|
|
4
|
+
export declare const GetChannelsSchema: z.ZodObject<{
|
|
5
|
+
include_private: z.ZodDefault<z.ZodBoolean>;
|
|
6
|
+
include_archived: z.ZodDefault<z.ZodBoolean>;
|
|
7
|
+
}, "strip", z.ZodTypeAny, {
|
|
8
|
+
include_private: boolean;
|
|
9
|
+
include_archived: boolean;
|
|
10
|
+
}, {
|
|
11
|
+
include_private?: boolean | undefined;
|
|
12
|
+
include_archived?: boolean | undefined;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function getChannelsTool(server: Server, clientFactory: ClientFactory): {
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
inputSchema: {
|
|
18
|
+
type: "object";
|
|
19
|
+
properties: {
|
|
20
|
+
include_private: {
|
|
21
|
+
type: string;
|
|
22
|
+
default: boolean;
|
|
23
|
+
description: string;
|
|
24
|
+
};
|
|
25
|
+
include_archived: {
|
|
26
|
+
type: string;
|
|
27
|
+
default: boolean;
|
|
28
|
+
description: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
required: never[];
|
|
32
|
+
};
|
|
33
|
+
handler: (args: unknown) => Promise<{
|
|
34
|
+
content: {
|
|
35
|
+
type: string;
|
|
36
|
+
text: string;
|
|
37
|
+
}[];
|
|
38
|
+
isError?: undefined;
|
|
39
|
+
} | {
|
|
40
|
+
content: {
|
|
41
|
+
type: string;
|
|
42
|
+
text: string;
|
|
43
|
+
}[];
|
|
44
|
+
isError: boolean;
|
|
45
|
+
}>;
|
|
46
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const PARAM_DESCRIPTIONS = {
|
|
3
|
+
include_private: 'Include private channels the bot is a member of. Default: true. ' +
|
|
4
|
+
'Set to false to only list public channels.',
|
|
5
|
+
include_archived: 'Include archived channels. Default: false. ' +
|
|
6
|
+
'Archived channels are read-only and cannot receive new messages.',
|
|
7
|
+
};
|
|
8
|
+
export const GetChannelsSchema = z.object({
|
|
9
|
+
include_private: z.boolean().default(true).describe(PARAM_DESCRIPTIONS.include_private),
|
|
10
|
+
include_archived: z.boolean().default(false).describe(PARAM_DESCRIPTIONS.include_archived),
|
|
11
|
+
});
|
|
12
|
+
const TOOL_DESCRIPTION = `List all Slack channels the bot has access to.
|
|
13
|
+
|
|
14
|
+
Returns a list of public and private channels in the workspace that the bot can see. Each channel includes its ID, name, topic, purpose, and member count.
|
|
15
|
+
|
|
16
|
+
**Returns:**
|
|
17
|
+
A formatted list of channels with their key details including:
|
|
18
|
+
- Channel ID (needed for other operations)
|
|
19
|
+
- Channel name
|
|
20
|
+
- Topic and purpose
|
|
21
|
+
- Whether the channel is private or archived
|
|
22
|
+
- Number of members
|
|
23
|
+
|
|
24
|
+
**Use cases:**
|
|
25
|
+
- Discover available channels to read or post to
|
|
26
|
+
- Find a specific channel's ID for subsequent operations
|
|
27
|
+
- Get an overview of the workspace's channel structure
|
|
28
|
+
|
|
29
|
+
**Note:** The bot can only see channels it has been invited to (for private channels) or all public channels.`;
|
|
30
|
+
export function getChannelsTool(server, clientFactory) {
|
|
31
|
+
return {
|
|
32
|
+
name: 'slack_get_channels',
|
|
33
|
+
description: TOOL_DESCRIPTION,
|
|
34
|
+
inputSchema: {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
include_private: {
|
|
38
|
+
type: 'boolean',
|
|
39
|
+
default: true,
|
|
40
|
+
description: PARAM_DESCRIPTIONS.include_private,
|
|
41
|
+
},
|
|
42
|
+
include_archived: {
|
|
43
|
+
type: 'boolean',
|
|
44
|
+
default: false,
|
|
45
|
+
description: PARAM_DESCRIPTIONS.include_archived,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
required: [],
|
|
49
|
+
},
|
|
50
|
+
handler: async (args) => {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = GetChannelsSchema.parse(args ?? {});
|
|
53
|
+
const client = clientFactory();
|
|
54
|
+
const types = parsed.include_private ? 'public_channel,private_channel' : 'public_channel';
|
|
55
|
+
const channels = await client.getChannels({
|
|
56
|
+
types,
|
|
57
|
+
excludeArchived: !parsed.include_archived,
|
|
58
|
+
});
|
|
59
|
+
if (channels.length === 0) {
|
|
60
|
+
return {
|
|
61
|
+
content: [
|
|
62
|
+
{
|
|
63
|
+
type: 'text',
|
|
64
|
+
text: 'No channels found. The bot may not have access to any channels yet.',
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const channelList = channels
|
|
70
|
+
.map((ch) => {
|
|
71
|
+
const flags = [];
|
|
72
|
+
if (ch.is_private)
|
|
73
|
+
flags.push('private');
|
|
74
|
+
if (ch.is_archived)
|
|
75
|
+
flags.push('archived');
|
|
76
|
+
if (ch.is_general)
|
|
77
|
+
flags.push('general');
|
|
78
|
+
const flagStr = flags.length > 0 ? ` [${flags.join(', ')}]` : '';
|
|
79
|
+
const topic = ch.topic?.value ? `\n Topic: ${ch.topic.value}` : '';
|
|
80
|
+
const purpose = ch.purpose?.value ? `\n Purpose: ${ch.purpose.value}` : '';
|
|
81
|
+
const members = ch.num_members !== undefined ? `\n Members: ${ch.num_members}` : '';
|
|
82
|
+
return `• #${ch.name}${flagStr}\n ID: ${ch.id}${topic}${purpose}${members}`;
|
|
83
|
+
})
|
|
84
|
+
.join('\n\n');
|
|
85
|
+
return {
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: 'text',
|
|
89
|
+
text: `Found ${channels.length} channel(s):\n\n${channelList}`,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
return {
|
|
96
|
+
content: [
|
|
97
|
+
{
|
|
98
|
+
type: 'text',
|
|
99
|
+
text: `Error fetching channels: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
isError: true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|