From fb3f8ee571daae580605f64b8e51555d41bdd7d3 Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Tue, 19 Nov 2024 13:38:28 +0000 Subject: [PATCH] Port over Slack server --- src/slack/index.ts | 615 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 615 insertions(+) create mode 100644 src/slack/index.ts diff --git a/src/slack/index.ts b/src/slack/index.ts new file mode 100644 index 00000000..6dc8051d --- /dev/null +++ b/src/slack/index.ts @@ -0,0 +1,615 @@ +#!/usr/bin/env node +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequest, + CallToolRequestSchema, + ListToolsRequestSchema, + Tool, +} from "@modelcontextprotocol/sdk/types.js"; + +// Type definitions for tool arguments +interface ListChannelsArgs { + limit?: number; + cursor?: string; +} + +interface PostMessageArgs { + channel_id: string; + text: string; +} + +interface ReplyToThreadArgs { + channel_id: string; + thread_ts: string; + text: string; +} + +interface AddReactionArgs { + channel_id: string; + timestamp: string; + reaction: string; +} + +interface GetChannelHistoryArgs { + channel_id: string; + limit?: number; +} + +interface GetThreadRepliesArgs { + channel_id: string; + thread_ts: string; +} + +interface SearchMessagesArgs { + query: string; + count?: number; +} + +interface GetUsersArgs { + cursor?: string; + limit?: number; +} + +interface GetUserProfileArgs { + user_id: string; +} + +// Tool definitions +const listChannelsTool: Tool = { + name: "slack_list_channels", + description: "List public channels in the workspace with pagination", + inputSchema: { + type: "object", + properties: { + limit: { + type: "number", + description: + "Maximum number of channels to return (default 100, max 200)", + default: 100, + }, + cursor: { + type: "string", + description: "Pagination cursor for next page of results", + }, + }, + }, +}; + +const postMessageTool: Tool = { + name: "slack_post_message", + description: "Post a new message to a Slack channel", + inputSchema: { + type: "object", + properties: { + channel_id: { + type: "string", + description: "The ID of the channel to post to", + }, + text: { + type: "string", + description: "The message text to post", + }, + }, + required: ["channel_id", "text"], + }, +}; + +const replyToThreadTool: Tool = { + name: "slack_reply_to_thread", + description: "Reply to a specific message thread in Slack", + inputSchema: { + type: "object", + properties: { + channel_id: { + type: "string", + description: "The ID of the channel containing the thread", + }, + thread_ts: { + type: "string", + description: "The timestamp of the parent message", + }, + text: { + type: "string", + description: "The reply text", + }, + }, + required: ["channel_id", "thread_ts", "text"], + }, +}; + +const addReactionTool: Tool = { + name: "slack_add_reaction", + description: "Add a reaction emoji to a message", + inputSchema: { + type: "object", + properties: { + channel_id: { + type: "string", + description: "The ID of the channel containing the message", + }, + timestamp: { + type: "string", + description: "The timestamp of the message to react to", + }, + reaction: { + type: "string", + description: "The name of the emoji reaction (without ::)", + }, + }, + required: ["channel_id", "timestamp", "reaction"], + }, +}; + +const getChannelHistoryTool: Tool = { + name: "slack_get_channel_history", + description: "Get recent messages from a channel", + inputSchema: { + type: "object", + properties: { + channel_id: { + type: "string", + description: "The ID of the channel", + }, + limit: { + type: "number", + description: "Number of messages to retrieve (default 10)", + default: 10, + }, + }, + required: ["channel_id"], + }, +}; + +const getThreadRepliesTool: Tool = { + name: "slack_get_thread_replies", + description: "Get all replies in a message thread", + inputSchema: { + type: "object", + properties: { + channel_id: { + type: "string", + description: "The ID of the channel containing the thread", + }, + thread_ts: { + type: "string", + description: "The timestamp of the parent message", + }, + }, + required: ["channel_id", "thread_ts"], + }, +}; + +const searchMessagesTool: Tool = { + name: "slack_search_messages", + description: "Search for messages across channels", + inputSchema: { + type: "object", + properties: { + query: { + type: "string", + description: "The search query", + }, + count: { + type: "number", + description: "Number of results to return (default 5)", + default: 5, + }, + }, + required: ["query"], + }, +}; + +const getUsersTool: Tool = { + name: "slack_get_users", + description: + "Get a list of all users in the workspace with their basic profile information", + inputSchema: { + type: "object", + properties: { + cursor: { + type: "string", + description: "Pagination cursor for next page of results", + }, + limit: { + type: "number", + description: "Maximum number of users to return (default 100, max 200)", + default: 100, + }, + }, + }, +}; + +const getUserProfileTool: Tool = { + name: "slack_get_user_profile", + description: "Get detailed profile information for a specific user", + inputSchema: { + type: "object", + properties: { + user_id: { + type: "string", + description: "The ID of the user", + }, + }, + required: ["user_id"], + }, +}; + +class SlackClient { + private botHeaders: { Authorization: string; "Content-Type": string }; + private userHeaders: { Authorization: string; "Content-Type": string }; + + constructor(botToken: string, userToken: string) { + this.botHeaders = { + Authorization: `Bearer ${botToken}`, + "Content-Type": "application/json", + }; + this.userHeaders = { + Authorization: `Bearer ${userToken}`, + "Content-Type": "application/json", + }; + } + + async getChannels(limit: number = 100, cursor?: string): Promise { + const params = new URLSearchParams({ + types: "public_channel", + exclude_archived: "true", + limit: Math.min(limit, 200).toString(), + team_id: process.env.SLACK_TEAM_ID!, + }); + + if (cursor) { + params.append("cursor", cursor); + } + + const response = await fetch( + `https://slack.com/api/conversations.list?${params}`, + { headers: this.botHeaders }, + ); + + return response.json(); + } + + async postMessage(channel_id: string, text: string): Promise { + const response = await fetch("https://slack.com/api/chat.postMessage", { + method: "POST", + headers: this.botHeaders, + body: JSON.stringify({ + channel: channel_id, + text: text, + }), + }); + + return response.json(); + } + + async postReply( + channel_id: string, + thread_ts: string, + text: string, + ): Promise { + const response = await fetch("https://slack.com/api/chat.postMessage", { + method: "POST", + headers: this.botHeaders, + body: JSON.stringify({ + channel: channel_id, + thread_ts: thread_ts, + text: text, + }), + }); + + return response.json(); + } + + async addReaction( + channel_id: string, + timestamp: string, + reaction: string, + ): Promise { + const response = await fetch("https://slack.com/api/reactions.add", { + method: "POST", + headers: this.botHeaders, + body: JSON.stringify({ + channel: channel_id, + timestamp: timestamp, + name: reaction, + }), + }); + + return response.json(); + } + + async getChannelHistory( + channel_id: string, + limit: number = 10, + ): Promise { + const params = new URLSearchParams({ + channel: channel_id, + limit: limit.toString(), + }); + + const response = await fetch( + `https://slack.com/api/conversations.history?${params}`, + { headers: this.botHeaders }, + ); + + return response.json(); + } + + async getThreadReplies(channel_id: string, thread_ts: string): Promise { + const params = new URLSearchParams({ + channel: channel_id, + ts: thread_ts, + }); + + const response = await fetch( + `https://slack.com/api/conversations.replies?${params}`, + { headers: this.botHeaders }, + ); + + return response.json(); + } + + async searchMessages(query: string, count: number = 5): Promise { + const params = new URLSearchParams({ + query: query, + count: count.toString(), + }); + + const response = await fetch( + `https://slack.com/api/search.messages?${params}`, + { headers: this.userHeaders }, + ); + + return response.json(); + } + + async getUsers(limit: number = 100, cursor?: string): Promise { + const params = new URLSearchParams({ + limit: Math.min(limit, 200).toString(), + team_id: process.env.SLACK_TEAM_ID!, + }); + + if (cursor) { + params.append("cursor", cursor); + } + + const response = await fetch(`https://slack.com/api/users.list?${params}`, { + headers: this.botHeaders, + }); + + return response.json(); + } + + async getUserProfile(user_id: string): Promise { + const params = new URLSearchParams({ + user: user_id, + include_labels: "true", + }); + + const response = await fetch( + `https://slack.com/api/users.profile.get?${params}`, + { headers: this.botHeaders }, + ); + + return response.json(); + } +} + +async function main() { + const botToken = process.env.SLACK_BOT_TOKEN; + const userToken = process.env.SLACK_USER_TOKEN; + const teamId = process.env.SLACK_TEAM_ID; + + if (!botToken || !userToken || !teamId) { + console.error( + "Please set SLACK_BOT_TOKEN, SLACK_USER_TOKEN, and SLACK_TEAM_ID environment variables", + ); + process.exit(1); + } + + console.error("Starting Slack MCP Server..."); + const server = new Server( + { + name: "Slack MCP Server", + version: "1.0.0", + }, + { + capabilities: { + tools: {}, + }, + }, + ); + + const slackClient = new SlackClient(botToken, userToken); + + server.setRequestHandler( + CallToolRequestSchema, + async (request: CallToolRequest) => { + console.error("Received CallToolRequest:", request); + try { + if (!request.params.arguments) { + throw new Error("No arguments provided"); + } + + switch (request.params.name) { + case "slack_list_channels": { + const args = request.params + .arguments as unknown as ListChannelsArgs; + const response = await slackClient.getChannels( + args.limit, + args.cursor, + ); + return { + content: [{ type: "text", text: JSON.stringify(response) }], + }; + } + + case "slack_post_message": { + const args = request.params.arguments as unknown as PostMessageArgs; + if (!args.channel_id || !args.text) { + throw new Error( + "Missing required arguments: channel_id and text", + ); + } + const response = await slackClient.postMessage( + args.channel_id, + args.text, + ); + return { + content: [{ type: "text", text: JSON.stringify(response) }], + }; + } + + case "slack_reply_to_thread": { + const args = request.params + .arguments as unknown as ReplyToThreadArgs; + if (!args.channel_id || !args.thread_ts || !args.text) { + throw new Error( + "Missing required arguments: channel_id, thread_ts, and text", + ); + } + const response = await slackClient.postReply( + args.channel_id, + args.thread_ts, + args.text, + ); + return { + content: [{ type: "text", text: JSON.stringify(response) }], + }; + } + + case "slack_add_reaction": { + const args = request.params.arguments as unknown as AddReactionArgs; + if (!args.channel_id || !args.timestamp || !args.reaction) { + throw new Error( + "Missing required arguments: channel_id, timestamp, and reaction", + ); + } + const response = await slackClient.addReaction( + args.channel_id, + args.timestamp, + args.reaction, + ); + return { + content: [{ type: "text", text: JSON.stringify(response) }], + }; + } + + case "slack_get_channel_history": { + const args = request.params + .arguments as unknown as GetChannelHistoryArgs; + if (!args.channel_id) { + throw new Error("Missing required argument: channel_id"); + } + const response = await slackClient.getChannelHistory( + args.channel_id, + args.limit, + ); + return { + content: [{ type: "text", text: JSON.stringify(response) }], + }; + } + + case "slack_get_thread_replies": { + const args = request.params + .arguments as unknown as GetThreadRepliesArgs; + if (!args.channel_id || !args.thread_ts) { + throw new Error( + "Missing required arguments: channel_id and thread_ts", + ); + } + const response = await slackClient.getThreadReplies( + args.channel_id, + args.thread_ts, + ); + return { + content: [{ type: "text", text: JSON.stringify(response) }], + }; + } + + case "slack_search_messages": { + const args = request.params + .arguments as unknown as SearchMessagesArgs; + if (!args.query) { + throw new Error("Missing required argument: query"); + } + const response = await slackClient.searchMessages( + args.query, + args.count, + ); + return { + content: [{ type: "text", text: JSON.stringify(response) }], + }; + } + + case "slack_get_users": { + const args = request.params.arguments as unknown as GetUsersArgs; + const response = await slackClient.getUsers( + args.limit, + args.cursor, + ); + return { + content: [{ type: "text", text: JSON.stringify(response) }], + }; + } + + case "slack_get_user_profile": { + const args = request.params + .arguments as unknown as GetUserProfileArgs; + if (!args.user_id) { + throw new Error("Missing required argument: user_id"); + } + const response = await slackClient.getUserProfile(args.user_id); + return { + content: [{ type: "text", text: JSON.stringify(response) }], + }; + } + + default: + throw new Error(`Unknown tool: ${request.params.name}`); + } + } catch (error) { + console.error("Error executing tool:", error); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + error: error instanceof Error ? error.message : String(error), + }), + }, + ], + }; + } + }, + ); + + server.setRequestHandler(ListToolsRequestSchema, async () => { + console.error("Received ListToolsRequest"); + return { + tools: [ + listChannelsTool, + postMessageTool, + replyToThreadTool, + addReactionTool, + getChannelHistoryTool, + getThreadRepliesTool, + searchMessagesTool, + getUsersTool, + getUserProfileTool, + ], + }; + }); + + const transport = new StdioServerTransport(); + console.error("Connecting server to transport..."); + await server.connect(transport); + + console.error("Slack MCP Server running on stdio"); +} + +main().catch((error) => { + console.error("Fatal error in main():", error); + process.exit(1); +});