server.ts•14.1 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { Dependencies } from "./config/dependencies.js";
export const dependencies = new Dependencies();
// Helper function to initialize with token parameter
async function initializeWithTokenParam(token?: string) {
if (!token) {
return {
content: [
{
type: "text" as const,
text: "❌ SLACK_TOKEN parameter is required. Please provide your Slack bot token.",
},
],
};
}
const success = await dependencies.initializeWithToken(token);
if (!success) {
return {
content: [
{
type: "text" as const,
text: "❌ Failed to initialize with provided token. Please check your Slack token.",
},
],
};
}
return null; // Success
}
function createServer(): McpServer {
const server = new McpServer({
name: "SlackMCPServer",
version: "1.0.0",
capabilities: {
resources: {},
tools: {},
},
});
// List channels tool
server.tool(
"slack_list_channels",
"List public channels in the workspace with pagination",
{
limit: z
.number()
.optional()
.describe(
"Maximum number of channels to return (default 100, max 200)"
),
cursor: z
.string()
.optional()
.describe("Pagination cursor for next page of results"),
token: z.string().describe("Slack bot token (e.g., xoxb-...)"),
},
async ({ limit, cursor, token }) => {
try {
const initCheck = await initializeWithTokenParam(token);
if (initCheck) return initCheck;
const result = await dependencies.listChannelsUseCase.execute({
limit,
cursor,
});
return result;
} catch (error) {
return {
content: [
{
type: "text" as const,
text: `❌ Error listing channels: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
};
}
}
);
// Post message tool
server.tool(
"slack_post_message",
"Post a new message to a Slack channel",
{
channel_id: z.string().describe("The ID of the channel to post to"),
text: z.string().describe("The message text to post"),
token: z.string().describe("Slack bot token (e.g., xoxb-...)"),
},
async ({ channel_id, text, token }) => {
try {
const initCheck = await initializeWithTokenParam(token);
if (initCheck) return initCheck;
const result = await dependencies.postMessageUseCase.execute({
channel_id,
text,
});
return result;
} catch (error) {
return {
content: [
{
type: "text" as const,
text: `❌ Error posting message: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
};
}
}
);
// Reply to thread tool
server.tool(
"slack_reply_to_thread",
"Reply to a specific message thread in Slack",
{
channel_id: z
.string()
.describe("The ID of the channel containing the thread"),
thread_ts: z
.string()
.describe(
"The timestamp of the parent message in the format '1234567890.123456'. Timestamps in the format without the period can be converted by adding the period such that 6 numbers come after it."
),
text: z.string().describe("The reply text"),
token: z.string().describe("Slack bot token (e.g., xoxb-...)"),
},
async ({ channel_id, thread_ts, text, token }) => {
try {
const initCheck = await initializeWithTokenParam(token);
if (initCheck) return initCheck;
const result = await dependencies.slackService.replyToThread({
channel_id,
thread_ts,
text,
});
if (!result.success) {
return {
content: [
{
type: "text" as const,
text: `❌ Failed to reply to thread: ${result.error}`,
},
],
};
}
return {
content: [
{
type: "text" as const,
text: `✅ **Reply posted successfully!** 🧵\n\n**Channel**: <#${channel_id}>\n**Thread**: ${thread_ts}\n**Content**: ${text}`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text" as const,
text: `❌ Error replying to thread: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
};
}
}
);
// Add reaction tool
server.tool(
"slack_add_reaction",
"Add a reaction emoji to a message",
{
channel_id: z
.string()
.describe("The ID of the channel containing the message"),
timestamp: z
.string()
.describe("The timestamp of the message to react to"),
reaction: z
.string()
.describe("The name of the emoji reaction (without ::)"),
token: z.string().describe("Slack bot token (e.g., xoxb-...)"),
},
async ({ channel_id, timestamp, reaction, token }) => {
try {
const initCheck = await initializeWithTokenParam(token);
if (initCheck) return initCheck;
const result = await dependencies.slackService.addReaction({
channel_id,
timestamp,
reaction,
});
if (!result.success) {
return {
content: [
{
type: "text" as const,
text: `❌ Failed to add reaction: ${result.error}`,
},
],
};
}
return {
content: [
{
type: "text" as const,
text: `✅ **Reaction added successfully!** :${reaction}:\n\n**Channel**: <#${channel_id}>\n**Message**: ${timestamp}`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text" as const,
text: `❌ Error adding reaction: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
};
}
}
);
// Get channel history tool
server.tool(
"slack_get_channel_history",
"Get recent messages from a channel",
{
channel_id: z.string().describe("The ID of the channel"),
limit: z
.number()
.optional()
.describe("Number of messages to retrieve (default 10)"),
token: z.string().describe("Slack bot token (e.g., xoxb-...)"),
},
async ({ channel_id, limit, token }) => {
try {
const initCheck = await initializeWithTokenParam(token);
if (initCheck) return initCheck;
const result = await dependencies.getChannelHistoryUseCase.execute({
channel_id,
limit,
});
return result;
} catch (error) {
return {
content: [
{
type: "text" as const,
text: `❌ Error getting channel history: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
};
}
}
);
// Get thread replies tool
server.tool(
"slack_get_thread_replies",
"Get all replies in a message thread",
{
channel_id: z
.string()
.describe("The ID of the channel containing the thread"),
thread_ts: z
.string()
.describe(
"The timestamp of the parent message in the format '1234567890.123456'. Timestamps in the format without the period can be converted by adding the period such that 6 numbers come after it."
),
token: z.string().describe("Slack bot token (e.g., xoxb-...)"),
},
async ({ channel_id, thread_ts, token }) => {
try {
const initCheck = await initializeWithTokenParam(token);
if (initCheck) return initCheck;
const result = await dependencies.slackService.getThreadReplies({
channel_id,
thread_ts,
});
if (!result.success) {
return {
content: [
{
type: "text" as const,
text: `❌ Failed to get thread replies: ${result.error}`,
},
],
};
}
const { messages } = result.data!;
if (messages.length === 0) {
return {
content: [
{
type: "text" as const,
text: `📭 No replies found in thread \`${thread_ts}\`.`,
},
],
};
}
const replyList = messages
.map((message, index) => {
const timestamp = new Date(
parseFloat(message.ts) * 1000
).toLocaleString();
return `**${index + 1}.** <@${message.user}> - ${timestamp}\n ${
message.text
}`;
})
.join("\n\n");
return {
content: [
{
type: "text" as const,
text: `🧵 **Thread Replies** (${messages.length} messages)\n**Channel**: <#${channel_id}>\n**Thread**: ${thread_ts}\n\n${replyList}`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text" as const,
text: `❌ Error getting thread replies: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
};
}
}
);
// Parse Slack URL and get thread details
server.tool(
"slack_parse_url",
"AUTOMATICALLY parse Slack URLs to get thread/message content. Use this IMMEDIATELY when user shares a Slack URL instead of asking them to copy-paste content.",
{
url: z
.string()
.describe(
"Slack URL (e.g., https://workspace.slack.com/archives/CHANNEL_ID/pTIMESTAMP)"
),
token: z.string().describe("Slack bot token (e.g., xoxb-...)"),
},
async ({ url, token }) => {
try {
const initCheck = await initializeWithTokenParam(token);
if (initCheck) return initCheck;
// Parse Slack URL - supports various formats
const urlPattern =
/https:\/\/([^.]+)\.slack\.com\/archives\/([^\/]+)\/p(\d+)(\d{6})?/;
const match = url.match(urlPattern);
if (!match) {
return {
content: [
{
type: "text" as const,
text: "❌ Invalid Slack URL format. Expected format: https://workspace.slack.com/archives/CHANNEL_ID/pTIMESTAMP",
},
],
};
}
const [, workspace, channelId, timestamp, microseconds] = match;
const messageTs = microseconds
? `${timestamp}.${microseconds}`
: `${timestamp}.000000`;
// First, get channel info
const channelsResult = await dependencies.listChannelsUseCase.execute(
{}
);
if (!channelsResult.content || channelsResult.content.length === 0) {
return {
content: [
{
type: "text" as const,
text: "❌ Could not fetch channels to find channel info",
},
],
};
}
const channelInfo = channelsResult.content[0].text.includes(channelId)
? channelsResult.content[0].text
: `Channel: ${channelId}`;
// Get channel history to find the specific message
const historyResult =
await dependencies.getChannelHistoryUseCase.execute({
channel_id: channelId,
limit: 100,
});
if (!historyResult.content || historyResult.content.length === 0) {
return {
content: [
{
type: "text" as const,
text: `❌ Could not fetch messages from channel ${channelId}`,
},
],
};
}
// Try to get thread replies if this is a threaded message
let threadInfo = "";
try {
const threadResult = await dependencies.slackService.getThreadReplies(
{
channel_id: channelId,
thread_ts: messageTs,
}
);
if (
threadResult.success &&
threadResult.data &&
threadResult.data.messages &&
threadResult.data.messages.length > 0
) {
threadInfo = `\n\n🧵 **Thread with ${threadResult.data.messages.length} replies:**\n`;
threadResult.data.messages.forEach((reply: any, index: number) => {
threadInfo += `${index + 1}. **${reply.user}**: ${reply.text}\n`;
});
}
} catch (error) {
// Thread might not exist, that's okay
}
return {
content: [
{
type: "text" as const,
text: `🔗 **Slack Message Details**
**Workspace**: ${workspace}
**Channel**: ${channelId}
**Message Timestamp**: ${messageTs}
**URL**: ${url}
📋 **Channel History:**
${historyResult.content[0].text}${threadInfo}
💡 *Use slack_get_thread_replies with channel_id="${channelId}" and thread_ts="${messageTs}" to get more thread details*`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text" as const,
text: `❌ Error parsing Slack URL: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
};
}
}
);
return server;
}
export default createServer;