Slack OpenAPI server

This commit is contained in:
Taylor Wilsdon 2025-04-17 15:39:27 -07:00
parent 134d4e6778
commit 77ab1cd9ec
6 changed files with 1002 additions and 0 deletions

51
servers/slack/Dockerfile Normal file
View File

@ -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

55
servers/slack/README.md Normal file
View File

@ -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

View File

@ -0,0 +1,7 @@
services:
server:
build:
context: .
ports:
- 8000:8000

301
servers/slack/main.py Normal file
View File

@ -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."}

View File

@ -0,0 +1,6 @@
fastapi
uvicorn[standard]
pydantic
python-multipart
httpx
python-dotenv

582
servers/slack/slack.ts Normal file
View File

@ -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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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);
});