Skip to content
Merged
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
2 changes: 1 addition & 1 deletion packages/slack-advanced/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@arvoretech/slack-advanced-mcp",
"version": "1.0.0",
"version": "1.1.0",
"description": "Advanced Slack MCP Server with semantic user search, smart DMs, style analysis, thread extraction, audio transcription, and image analysis",
"main": "dist/index.js",
"type": "module",
Expand Down
10 changes: 10 additions & 0 deletions packages/slack-advanced/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
TranscribeAudioParamsSchema,
AnalyzeImageParamsSchema,
GetFileInfoParamsSchema,
SendChannelMessageParamsSchema,
} from "./types.js";

export class SlackAdvancedMCPServer {
Expand Down Expand Up @@ -91,6 +92,15 @@ export class SlackAdvancedMCPServer {
return this.messagingTools.sendDm(SendDmParamsSchema.parse(params));
});

this.server.registerTool("send_channel_message", {
title: "Send Channel Message",
description:
"Send a message to a Slack channel. Accepts channel ID or #channel-name. Supports thread replies and markdown link conversion.",
inputSchema: SendChannelMessageParamsSchema.shape,
}, async (params) => {
return this.messagingTools.sendChannelMessage(SendChannelMessageParamsSchema.parse(params));
});

this.server.registerTool("get_dm_history", {
title: "Get DM History",
description:
Expand Down
30 changes: 30 additions & 0 deletions packages/slack-advanced/src/slack-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,36 @@ export class SlackClient {
);
}

async resolveChannelId(identifier: string): Promise<string> {
if (/^[A-Z0-9]+$/.test(identifier)) {
return identifier;
}

const name = identifier.replace(/^#/, "");

let cursor: string | undefined;
do {
const params: Record<string, unknown> = { limit: 200, types: "public_channel,private_channel" };
if (cursor) params.cursor = cursor;

const res = await this.request<{
ok: boolean;
channels: Array<{ id: string; name: string }>;
response_metadata?: { next_cursor?: string };
}>("conversations.list", params);

const match = res.channels.find((c) => c.name === name);
if (match) return match.id;

cursor = res.response_metadata?.next_cursor || undefined;
} while (cursor);

throw new SlackAdvancedMCPError(
`Could not resolve channel: ${identifier}`,
"CHANNEL_NOT_FOUND"
);
}

async openDm(userId: string): Promise<string> {
const res = await this.request<{
ok: boolean;
Expand Down
40 changes: 40 additions & 0 deletions packages/slack-advanced/src/tools/messaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SlackClient } from "../slack-client.js";
import type {
SendDmParams,
GetDmHistoryParams,
SendChannelMessageParams,
McpToolResult,
SlackMessage,
} from "../types.js";
Expand Down Expand Up @@ -82,6 +83,45 @@ export class MessagingTools {
}
}

async sendChannelMessage(params: SendChannelMessageParams): Promise<McpToolResult> {
try {
const channelId = await this.slack.resolveChannelId(params.channel);

let text = params.text;
if (params.content_type === "text/markdown") {
text = this.convertMarkdownLinksToMrkdwn(text);
}

const msgParams: Record<string, unknown> = {
channel: channelId,
text,
};

if (params.thread_ts) {
msgParams.thread_ts = params.thread_ts;
}

const res = await this.slack.request<{
ok: boolean;
channel: string;
ts: string;
message: { text: string; ts: string };
}>("chat.postMessage", msgParams);

return this.ok({
sent: true,
channel: res.channel,
ts: res.ts,
});
} catch (error) {
return this.formatError(error);
}
}

private convertMarkdownLinksToMrkdwn(text: string): string {
return text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "<$2|$1>");
}

private ok(data: unknown): McpToolResult {
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
Expand Down
20 changes: 20 additions & 0 deletions packages/slack-advanced/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,25 @@ export const GetFileInfoParamsSchema = z.object({
.describe("Slack file ID"),
});

export const SendChannelMessageParamsSchema = z.object({
channel: z
.string()
.min(1, "Channel is required")
.describe("Channel ID (e.g. C01EXAMPLE) or channel name (e.g. #general)"),
text: z
.string()
.min(1, "Message text is required")
.describe("Message content (supports Slack mrkdwn)"),
thread_ts: z
.string()
.optional()
.describe("Thread timestamp to reply in a thread"),
content_type: z
.enum(["text/plain", "text/markdown"])
.optional()
.describe("Content type for link formatting. Use text/markdown for [text](url) links"),
});

export type SearchUsersParams = z.infer<typeof SearchUsersParamsSchema>;
export type GetUserProfileParams = z.infer<typeof GetUserProfileParamsSchema>;
export type SendDmParams = z.infer<typeof SendDmParamsSchema>;
Expand All @@ -137,6 +156,7 @@ export type GetThreadFromLinkParams = z.infer<typeof GetThreadFromLinkParamsSche
export type TranscribeAudioParams = z.infer<typeof TranscribeAudioParamsSchema>;
export type AnalyzeImageParams = z.infer<typeof AnalyzeImageParamsSchema>;
export type GetFileInfoParams = z.infer<typeof GetFileInfoParamsSchema>;
export type SendChannelMessageParams = z.infer<typeof SendChannelMessageParamsSchema>;

export type McpTextContent = {
type: "text";
Expand Down
Loading