mirror of
https://github.com/open-webui/openapi-servers
synced 2025-06-26 18:17:04 +00:00
Slack OpenAPI server
This commit is contained in:
51
servers/slack/Dockerfile
Normal file
51
servers/slack/Dockerfile
Normal 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
55
servers/slack/README.md
Normal 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
|
||||||
7
servers/slack/compose.yaml
Normal file
7
servers/slack/compose.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
services:
|
||||||
|
server:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
ports:
|
||||||
|
- 8000:8000
|
||||||
|
|
||||||
301
servers/slack/main.py
Normal file
301
servers/slack/main.py
Normal 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."}
|
||||||
6
servers/slack/requirements.txt
Normal file
6
servers/slack/requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn[standard]
|
||||||
|
pydantic
|
||||||
|
python-multipart
|
||||||
|
httpx
|
||||||
|
python-dotenv
|
||||||
582
servers/slack/slack.ts
Normal file
582
servers/slack/slack.ts
Normal 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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user