Skip to content

Commit 0164d7a

Browse files
authored
feat(slack-advanced): add create_channel and create_group_dm tools (#74)
* feat(slack-advanced): add create_channel tool Add a create_channel tool that creates public or private Slack channels and optionally invites users by ID, email, or display name. Supports setting a topic and purpose, and reports per-user invite errors without failing the whole operation. * feat(slack-advanced): add create_group_dm tool Add a create_group_dm tool that opens a multi-person DM (MPDM) with multiple users referenced by ID, email, or display name, and optionally posts a message. Unresolved users are reported without aborting. Requires the mpim:write scope. * chore(slack-advanced): bump version to 1.11.0
1 parent f047f84 commit 0164d7a

4 files changed

Lines changed: 191 additions & 1 deletion

File tree

packages/slack-advanced/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@arvoretech/slack-advanced-mcp",
3-
"version": "1.10.0",
3+
"version": "1.11.0",
44
"description": "Advanced Slack MCP Server with semantic user search, smart DMs, style analysis, thread extraction, audio transcription, and image analysis",
55
"main": "dist/index.js",
66
"type": "module",

packages/slack-advanced/src/server.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import {
3030
DeleteMessageParamsSchema,
3131
AddReactionParamsSchema,
3232
RemoveReactionParamsSchema,
33+
CreateChannelParamsSchema,
34+
CreateGroupDmParamsSchema,
3335
} from "./types.js";
3436

3537
export class SlackAdvancedMCPServer {
@@ -258,6 +260,24 @@ export class SlackAdvancedMCPServer {
258260
}, async (params) => {
259261
return this.messagingTools.removeReaction(RemoveReactionParamsSchema.parse(params));
260262
});
263+
264+
this.server.registerTool("create_channel", {
265+
title: "Create Channel",
266+
description:
267+
"Create a new Slack channel (public or private) and optionally invite users to it. Users can be referenced by ID, email, or display name. Optionally sets a topic and purpose. Returns the new channel ID, invited users, and any invite errors. Requires channels:manage and/or groups:write scopes, plus channels:write.invites for inviting members.",
268+
inputSchema: CreateChannelParamsSchema.shape,
269+
}, async (params) => {
270+
return this.messagingTools.createChannel(CreateChannelParamsSchema.parse(params));
271+
});
272+
273+
this.server.registerTool("create_group_dm", {
274+
title: "Create Group DM",
275+
description:
276+
"Open a multi-person direct message (MPDM / group DM) with multiple users and optionally post a message in it. Users can be referenced by ID, email, or display name (the authenticated user is added automatically, up to 8 other members). Users that cannot be resolved are reported in resolve_errors without aborting the operation. Requires the mpim:write scope.",
277+
inputSchema: CreateGroupDmParamsSchema.shape,
278+
}, async (params) => {
279+
return this.messagingTools.createGroupDm(CreateGroupDmParamsSchema.parse(params));
280+
});
261281
}
262282

263283
async start(): Promise<void> {

packages/slack-advanced/src/tools/messaging.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import type {
99
DeleteMessageParams,
1010
AddReactionParams,
1111
RemoveReactionParams,
12+
CreateChannelParams,
13+
CreateGroupDmParams,
1214
McpToolResult,
1315
SlackMessage,
1416
} from "../types.js";
@@ -309,6 +311,137 @@ export class MessagingTools {
309311
}
310312
}
311313

314+
async createChannel(params: CreateChannelParams): Promise<McpToolResult> {
315+
try {
316+
const createRes = await this.slack.request<{
317+
ok: boolean;
318+
channel: { id: string; name: string; is_private: boolean };
319+
}>("conversations.create", {
320+
name: params.name,
321+
is_private: params.is_private,
322+
});
323+
324+
const channelId = createRes.channel.id;
325+
const invited: Array<{ user: string; user_id: string }> = [];
326+
const inviteErrors: Array<{ user: string; error: string }> = [];
327+
328+
if (params.invite_users && params.invite_users.length > 0) {
329+
const userIds: string[] = [];
330+
for (const identifier of params.invite_users) {
331+
try {
332+
const userId = await this.slack.resolveUserId(identifier);
333+
userIds.push(userId);
334+
invited.push({ user: identifier, user_id: userId });
335+
} catch (error) {
336+
inviteErrors.push({
337+
user: identifier,
338+
error: error instanceof Error ? error.message : "Could not resolve user",
339+
});
340+
}
341+
}
342+
343+
if (userIds.length > 0) {
344+
try {
345+
await this.slack.request<{ ok: boolean }>("conversations.invite", {
346+
channel: channelId,
347+
users: userIds.join(","),
348+
});
349+
} catch (error) {
350+
inviteErrors.push({
351+
user: userIds.join(","),
352+
error: error instanceof Error ? error.message : "Failed to invite users",
353+
});
354+
}
355+
}
356+
}
357+
358+
if (params.topic) {
359+
await this.slack.request<{ ok: boolean }>("conversations.setTopic", {
360+
channel: channelId,
361+
topic: params.topic,
362+
});
363+
}
364+
365+
if (params.purpose) {
366+
await this.slack.request<{ ok: boolean }>("conversations.setPurpose", {
367+
channel: channelId,
368+
purpose: params.purpose,
369+
});
370+
}
371+
372+
return this.ok({
373+
created: true,
374+
channel_id: channelId,
375+
name: createRes.channel.name,
376+
is_private: createRes.channel.is_private,
377+
invited,
378+
...(inviteErrors.length > 0 && { invite_errors: inviteErrors }),
379+
});
380+
} catch (error) {
381+
return this.formatError(error);
382+
}
383+
}
384+
385+
async createGroupDm(params: CreateGroupDmParams): Promise<McpToolResult> {
386+
try {
387+
const resolved: Array<{ user: string; user_id: string }> = [];
388+
const resolveErrors: Array<{ user: string; error: string }> = [];
389+
390+
for (const identifier of params.users) {
391+
try {
392+
const userId = await this.slack.resolveUserId(identifier);
393+
resolved.push({ user: identifier, user_id: userId });
394+
} catch (error) {
395+
resolveErrors.push({
396+
user: identifier,
397+
error: error instanceof Error ? error.message : "Could not resolve user",
398+
});
399+
}
400+
}
401+
402+
if (resolved.length === 0) {
403+
return this.ok({
404+
created: false,
405+
error: "No users could be resolved",
406+
resolve_errors: resolveErrors,
407+
});
408+
}
409+
410+
const openRes = await this.slack.request<{
411+
ok: boolean;
412+
channel: { id: string };
413+
}>("conversations.open", {
414+
users: resolved.map((r) => r.user_id).join(","),
415+
});
416+
417+
const channelId = openRes.channel.id;
418+
let messageTs: string | null = null;
419+
420+
if (params.message) {
421+
const mrkdwn = this.toMrkdwn(params.message);
422+
const blocks = this.buildBlocks(mrkdwn);
423+
const res = await this.slack.request<{ ok: boolean; ts: string }>("chat.postMessage", {
424+
channel: channelId,
425+
text: mrkdwn,
426+
blocks: JSON.stringify(blocks),
427+
unfurl_links: false,
428+
unfurl_media: false,
429+
});
430+
messageTs = res.ts;
431+
}
432+
433+
return this.ok({
434+
created: true,
435+
channel_id: channelId,
436+
members: resolved,
437+
...(messageTs && { message_ts: messageTs }),
438+
...(resolveErrors.length > 0 && { resolve_errors: resolveErrors }),
439+
});
440+
} catch (error) {
441+
return this.formatError(error);
442+
}
443+
}
444+
312445
private ok(data: unknown): McpToolResult {
313446
return {
314447
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],

packages/slack-advanced/src/types.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,9 +387,46 @@ export const GetUserInfoParamsSchema = z.object({
387387
.describe("User ID, email, display name, or real name to look up"),
388388
});
389389

390+
export const CreateChannelParamsSchema = z.object({
391+
name: z
392+
.string()
393+
.min(1, "Channel name is required")
394+
.describe("Channel name. Slack normalizes it to lowercase, replaces spaces with hyphens, and strips invalid characters (max 80 chars)"),
395+
is_private: z
396+
.boolean()
397+
.optional()
398+
.default(false)
399+
.describe("Create a private channel instead of a public one"),
400+
invite_users: z
401+
.array(z.string().min(1))
402+
.optional()
403+
.describe("List of users to invite after creation. Each entry can be a user ID, email, or display name"),
404+
topic: z
405+
.string()
406+
.optional()
407+
.describe("Optional channel topic to set after creation"),
408+
purpose: z
409+
.string()
410+
.optional()
411+
.describe("Optional channel purpose/description to set after creation"),
412+
});
413+
414+
export const CreateGroupDmParamsSchema = z.object({
415+
users: z
416+
.array(z.string().min(1))
417+
.min(1, "At least one user is required")
418+
.describe("Users to include in the multi-person DM (MPDM). Each entry can be a user ID, email, or display name. The authenticated user is added automatically by Slack. Slack supports up to 8 other members"),
419+
message: z
420+
.string()
421+
.optional()
422+
.describe("Optional message to post in the group DM right after opening it (supports Slack mrkdwn)"),
423+
});
424+
390425
export type SearchUsersParams = z.infer<typeof SearchUsersParamsSchema>;
391426
export type GetUserProfileParams = z.infer<typeof GetUserProfileParamsSchema>;
392427
export type GetUserInfoParams = z.infer<typeof GetUserInfoParamsSchema>;
428+
export type CreateChannelParams = z.infer<typeof CreateChannelParamsSchema>;
429+
export type CreateGroupDmParams = z.infer<typeof CreateGroupDmParamsSchema>;
393430
export type MessageMetadata = z.infer<typeof MessageMetadataSchema>;
394431
export type SendDmParams = z.infer<typeof SendDmParamsSchema>;
395432
export type GetDmHistoryParams = z.infer<typeof GetDmHistoryParamsSchema>;

0 commit comments

Comments
 (0)