diff --git a/servers/slack/Dockerfile b/servers/slack/Dockerfile new file mode 100644 index 0000000..e91fca9 --- /dev/null +++ b/servers/slack/Dockerfile @@ -0,0 +1,51 @@ +# syntax=docker/dockerfile:1 + +# Comments are provided throughout this file to help you get started. +# If you need more help, visit the Dockerfile reference guide at +# https://docs.docker.com/go/dockerfile-reference/ + +# Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7 + +ARG PYTHON_VERSION=3.10.12 +FROM python:${PYTHON_VERSION}-slim as base + +# Prevents Python from writing pyc files. +ENV PYTHONDONTWRITEBYTECODE=1 + +# Keeps Python from buffering stdout and stderr to avoid situations where +# the application crashes without emitting any logs due to buffering. +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +# Create a non-privileged user that the app will run under. +# See https://docs.docker.com/go/dockerfile-user-best-practices/ +ARG UID=10001 +RUN adduser \ + --disabled-password \ + --gecos "" \ + --home "/nonexistent" \ + --shell "/sbin/nologin" \ + --no-create-home \ + --uid "${UID}" \ + appuser + +# Download dependencies as a separate step to take advantage of Docker's caching. +# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds. +# Leverage a bind mount to requirements.txt to avoid having to copy them into +# into this layer. +RUN --mount=type=cache,target=/root/.cache/pip \ + --mount=type=bind,source=requirements.txt,target=requirements.txt \ + python -m pip install -r requirements.txt + +# Switch to the non-privileged user to run the application. +USER appuser + +# Copy the source code into the container. +COPY . . + +# Expose the port that the application listens on. +EXPOSE 8000 + +# Run the application. +CMD uvicorn 'main:app' --host=0.0.0.0 --port=8000 diff --git a/servers/slack/README.md b/servers/slack/README.md new file mode 100644 index 0000000..66f8523 --- /dev/null +++ b/servers/slack/README.md @@ -0,0 +1,55 @@ +# โ›… Weather Tool Server + +A sleek and simple FastAPI-based server to provide weather data using OpenAPI standards. + +๐Ÿ“ฆ Built with: +โšก๏ธ FastAPI โ€ข ๐Ÿ“œ OpenAPI โ€ข ๐Ÿงฐ Python + +--- + +## ๐Ÿš€ Quickstart + +Clone the repo and get started in seconds: + +```bash +git clone https://github.com/open-webui/openapi-servers +cd openapi-servers/servers/weather + +# Install dependencies +pip install -r requirements.txt + +# Run the server +uvicorn main:app --host 0.0.0.0 --reload +``` + +--- + +## ๐Ÿ” About + +This server is part of the OpenAPI Tools Collection. Use it to fetch real-time weather information, location-based forecasts, and more โ€” all wrapped in a developer-friendly OpenAPI interface. + +Compatible with any OpenAPI-supported ecosystem, including: + +- ๐ŸŒ€ FastAPI +- ๐Ÿ“˜ Swagger UI +- ๐Ÿงช API testing tools + +--- + +## ๐Ÿšง Customization + +Plug in your favorite weather provider API, tailor endpoints, or extend the OpenAPI spec. Ideal for integration into AI agents, automated dashboards, or personal assistants. + +--- + +## ๐ŸŒ API Documentation + +Once running, explore auto-generated interactive docs: + +๐Ÿ–ฅ๏ธ Swagger UI: http://localhost:8000/docs +๐Ÿ“„ OpenAPI JSON: http://localhost:8000/openapi.json + +--- + +Made with โค๏ธ by the Open WebUI community ๐ŸŒ +Explore more tools โžก๏ธ https://github.com/open-webui/openapi-servers \ No newline at end of file diff --git a/servers/slack/compose.yaml b/servers/slack/compose.yaml new file mode 100644 index 0000000..9fc4d98 --- /dev/null +++ b/servers/slack/compose.yaml @@ -0,0 +1,7 @@ +services: + server: + build: + context: . + ports: + - 8000:8000 + diff --git a/servers/slack/main.py b/servers/slack/main.py new file mode 100644 index 0000000..4b2f23c --- /dev/null +++ b/servers/slack/main.py @@ -0,0 +1,301 @@ +# [Previous imports and setup remain the same...] +import os +import httpx +import inspect +from typing import Optional, List, Dict, Any, Type +from fastapi import FastAPI, HTTPException, Body, Depends +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel, Field +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + +# --- Environment Variable Checks --- +SLACK_BOT_TOKEN = os.getenv("SLACK_BOT_TOKEN") +SLACK_TEAM_ID = os.getenv("SLACK_TEAM_ID") +SLACK_CHANNEL_IDS_STR = os.getenv("SLACK_CHANNEL_IDS") # Optional + +if not SLACK_BOT_TOKEN: + raise ValueError("SLACK_BOT_TOKEN environment variable not set.") +if not SLACK_TEAM_ID: + raise ValueError("SLACK_TEAM_ID environment variable not set.") + +PREDEFINED_CHANNEL_IDS = [ + channel_id.strip() + for channel_id in SLACK_CHANNEL_IDS_STR.split(',') +] if SLACK_CHANNEL_IDS_STR else None + +# --- FastAPI App Setup --- +app = FastAPI( + title="Slack API Server", + version="1.0.0", + description="FastAPI server providing Slack functionalities via specific, dynamically generated tool endpoints.", +) + +origins = ["*"] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# [Previous Pydantic models remain the same...] +class ListChannelsArgs(BaseModel): + limit: Optional[int] = Field(100, description="Maximum number of channels to return (default 100, max 200)") + cursor: Optional[str] = Field(None, description="Pagination cursor for next page of results") + +class PostMessageArgs(BaseModel): + channel_id: str = Field(..., description="The ID of the channel to post to") + text: str = Field(..., description="The message text to post") + +class ReplyToThreadArgs(BaseModel): + channel_id: str = Field(..., description="The ID of the channel containing the thread") + thread_ts: str = Field(..., description="The timestamp of the parent message (e.g., '1234567890.123456')") + text: str = Field(..., description="The reply text") + +class AddReactionArgs(BaseModel): + channel_id: str = Field(..., description="The ID of the channel containing the message") + timestamp: str = Field(..., description="The timestamp of the message to react to") + reaction: str = Field(..., description="The name of the emoji reaction (without colons)") + +class GetChannelHistoryArgs(BaseModel): + channel_id: str = Field(..., description="The ID of the channel") + limit: Optional[int] = Field(10, description="Number of messages to retrieve (default 10)") + +class GetThreadRepliesArgs(BaseModel): + channel_id: str = Field(..., description="The ID of the channel containing the thread") + thread_ts: str = Field(..., description="The timestamp of the parent message (e.g., '1234567890.123456')") + +class GetUsersArgs(BaseModel): + cursor: Optional[str] = Field(None, description="Pagination cursor for next page of results") + limit: Optional[int] = Field(100, description="Maximum number of users to return (default 100, max 200)") + +class GetUserProfileArgs(BaseModel): + user_id: str = Field(..., description="The ID of the user") + +class ToolResponse(BaseModel): + content: Dict[str, Any] = Field(..., description="The JSON response from the Slack API call") + +# --- Slack Client Class --- +class SlackClient: + BASE_URL = "https://slack.com/api/" + + def __init__(self, token: str, team_id: str): + self.headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json; charset=utf-8", + } + self.team_id = team_id + + async def _request(self, method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None) -> Dict[str, Any]: + async with httpx.AsyncClient(base_url=self.BASE_URL, headers=self.headers) as client: + try: + response = await client.request(method, endpoint, params=params, json=json_data) + response.raise_for_status() + data = response.json() + if not data.get("ok"): + error_msg = data.get("error", "Unknown Slack API error") + print(f"Slack API Error for {method} {endpoint}: {error_msg}") + raise HTTPException(status_code=400, detail={"slack_error": error_msg, "message": f"Slack API Error: {error_msg}"}) + return data + except httpx.HTTPStatusError as e: + print(f"HTTP Error: {e.response.status_code} - {e.response.text}") + raise HTTPException(status_code=e.response.status_code, detail=f"Slack API HTTP Error: {e.response.text}") + except httpx.RequestError as e: + print(f"Request Error: {e}") + raise HTTPException(status_code=503, detail=f"Error connecting to Slack API: {e}") + except Exception as e: + print(f"Unexpected Error during Slack request: {e}") + raise HTTPException(status_code=500, detail=f"An internal error occurred during the Slack request: {e}") + + async def get_channel_history(self, args: GetChannelHistoryArgs) -> Dict[str, Any]: + params = {"channel": args.channel_id, "limit": args.limit} + return await self._request("GET", "conversations.history", params=params) + + async def get_channels(self, args: ListChannelsArgs) -> Dict[str, Any]: + limit = args.limit + cursor = args.cursor + + async def fetch_channel_with_history(channel_id: str) -> Dict[str, Any]: + # First get channel info + channel_info = await self._request("GET", "conversations.info", params={"channel": channel_id}) + if not channel_info.get("ok") or channel_info.get("channel", {}).get("is_archived"): + return None + + channel_data = channel_info["channel"] + + # Then get channel history + try: + history = await self._request( + "GET", + "conversations.history", + params={ + "channel": channel_id, + "limit": 10 # Get last 10 messages by default + } + ) + # Add history to channel data + if history.get("ok"): + channel_data["history"] = history.get("messages", []) + except Exception as e: + print(f"Error fetching history for channel {channel_id}: {e}") + channel_data["history"] = [] + + return channel_data + + if PREDEFINED_CHANNEL_IDS: + channels_info = [] + for channel_id in PREDEFINED_CHANNEL_IDS: + try: + if channel_data := await fetch_channel_with_history(channel_id): + channels_info.append(channel_data) + except Exception as e: + print(f"Could not fetch info for predefined channel {channel_id}: {e}") + + return { + "ok": True, + "channels": channels_info, + "response_metadata": {"next_cursor": ""} + } + else: + # First get list of channels + params = { + "types": "public_channel", + "exclude_archived": "true", + "limit": min(limit, 200), + "team_id": self.team_id, + } + if cursor: + params["cursor"] = cursor + + channels_list = await self._request("GET", "conversations.list", params=params) + + if not channels_list.get("ok"): + return channels_list + + # Then fetch history for each channel + channels_with_history = [] + for channel in channels_list["channels"]: + try: + if channel_data := await fetch_channel_with_history(channel["id"]): + channels_with_history.append(channel_data) + except Exception as e: + print(f"Error fetching history for channel {channel['id']}: {e}") + channels_with_history.append(channel) # Fall back to channel info without history + + return { + "ok": True, + "channels": channels_with_history, + "response_metadata": channels_list.get("response_metadata", {"next_cursor": ""}) + } + + async def post_message(self, args: PostMessageArgs) -> Dict[str, Any]: + payload = {"channel": args.channel_id, "text": args.text} + return await self._request("POST", "chat.postMessage", json_data=payload) + + async def post_reply(self, args: ReplyToThreadArgs) -> Dict[str, Any]: + payload = {"channel": args.channel_id, "thread_ts": args.thread_ts, "text": args.text} + return await self._request("POST", "chat.postMessage", json_data=payload) + + async def add_reaction(self, args: AddReactionArgs) -> Dict[str, Any]: + payload = {"channel": args.channel_id, "timestamp": args.timestamp, "name": args.reaction} + return await self._request("POST", "reactions.add", json_data=payload) + + async def get_thread_replies(self, args: GetThreadRepliesArgs) -> Dict[str, Any]: + params = {"channel": args.channel_id, "ts": args.thread_ts} + return await self._request("GET", "conversations.replies", params=params) + + async def get_users(self, args: GetUsersArgs) -> Dict[str, Any]: + params = { + "limit": min(args.limit, 200), + "team_id": self.team_id, + } + if args.cursor: + params["cursor"] = args.cursor + return await self._request("GET", "users.list", params=params) + + async def get_user_profile(self, args: GetUserProfileArgs) -> Dict[str, Any]: + params = {"user": args.user_id, "include_labels": "true"} + return await self._request("GET", "users.profile.get", params=params) + +# --- Instantiate Slack Client --- +slack_client = SlackClient(token=SLACK_BOT_TOKEN, team_id=SLACK_TEAM_ID) + +# --- Tool Definitions & Endpoint Generation --- +TOOL_MAPPING = { + "slack_list_channels": { + "args_model": ListChannelsArgs, + "method": slack_client.get_channels, + "description": "List public or pre-defined channels in the workspace with pagination", + }, + "slack_post_message": { + "args_model": PostMessageArgs, + "method": slack_client.post_message, + "description": "Post a new message to a Slack channel", + }, + "slack_reply_to_thread": { + "args_model": ReplyToThreadArgs, + "method": slack_client.post_reply, + "description": "Reply to a specific message thread in Slack", + }, + "slack_add_reaction": { + "args_model": AddReactionArgs, + "method": slack_client.add_reaction, + "description": "Add a reaction emoji to a message", + }, + "slack_get_channel_history": { + "args_model": GetChannelHistoryArgs, + "method": slack_client.get_channel_history, + "description": "Get recent messages from a channel", + }, + "slack_get_thread_replies": { + "args_model": GetThreadRepliesArgs, + "method": slack_client.get_thread_replies, + "description": "Get all replies in a message thread", + }, + "slack_get_users": { + "args_model": GetUsersArgs, + "method": slack_client.get_users, + "description": "Get a list of all users in the workspace with their basic profile information", + }, + "slack_get_user_profile": { + "args_model": GetUserProfileArgs, + "method": slack_client.get_user_profile, + "description": "Get detailed profile information for a specific user", + }, +} + +# Dynamically create endpoints for each tool +for tool_name, config in TOOL_MAPPING.items(): + args_model = config["args_model"] + method_to_call = config["method"] + tool_description = config["description"] + + async def endpoint_func(args: args_model = Body(...), # type: ignore + method=method_to_call): # Capture method in closure + try: + result = await method(args=args) + return {"content": result} + except HTTPException as e: + raise e + except Exception as e: + print(f"Error executing tool: {e}") + raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") + + app.post( + f"/{tool_name}", + response_model=ToolResponse, + summary=tool_description, + description=f"Executes the {tool_name} tool. Arguments are passed in the request body.", + tags=["Slack Tools"], + name=tool_name + )(endpoint_func) + +# --- Root Endpoint --- +@app.get("/", summary="Root endpoint", include_in_schema=False) +async def read_root(): + return {"message": "Slack API Server is running. See /docs for available tool endpoints."} diff --git a/servers/slack/requirements.txt b/servers/slack/requirements.txt new file mode 100644 index 0000000..9d2bd0a --- /dev/null +++ b/servers/slack/requirements.txt @@ -0,0 +1,6 @@ +fastapi +uvicorn[standard] +pydantic +python-multipart +httpx +python-dotenv diff --git a/servers/slack/slack.ts b/servers/slack/slack.ts new file mode 100644 index 0000000..dde2c8b --- /dev/null +++ b/servers/slack/slack.ts @@ -0,0 +1,582 @@ +#!/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 GetUsersArgs { + cursor?: string; + limit?: number; +} + +interface GetUserProfileArgs { + user_id: string; +} + +// Tool definitions +const listChannelsTool: Tool = { + name: "slack_list_channels", + description: "List public or pre-defined 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 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: { + 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 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.", + }, + }, + required: ["channel_id", "thread_ts"], + }, +}; + +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 }; + + constructor(botToken: string) { + this.botHeaders = { + Authorization: `Bearer ${botToken}`, + "Content-Type": "application/json", + }; + } + + async getChannels(limit: number = 100, cursor?: string): Promise { + const predefinedChannelIds = process.env.SLACK_CHANNEL_IDS; + if (!predefinedChannelIds) { + 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(); + } + + const predefinedChannelIdsArray = predefinedChannelIds.split(",").map((id: string) => id.trim()); + const channels = []; + + for (const channelId of predefinedChannelIdsArray) { + const params = new URLSearchParams({ + channel: channelId, + }); + + const response = await fetch( + `https://slack.com/api/conversations.info?${params}`, + { headers: this.botHeaders } + ); + const data = await response.json(); + + if (data.ok && data.channel && !data.channel.is_archived) { + channels.push(data.channel); + } + } + + return { + ok: true, + channels: channels, + response_metadata: { next_cursor: "" }, + }; + } + + 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 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 teamId = process.env.SLACK_TEAM_ID; + + if (!botToken || !teamId) { + console.error( + "Please set SLACK_BOT_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); + + 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_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, + 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); +}); \ No newline at end of file