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:
parent
134d4e6778
commit
77ab1cd9ec
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);
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user