#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import { WebClient } from '@slack/web-api';
import { z } from 'zod';
import * as dotenv from 'dotenv';
import * as cron from 'node-cron';
import axios from 'axios';
// Carregar variáveis de ambiente
dotenv.config();
// Validação do schema para argumentos
const SlackMessageSchema = z.object({
channel: z.string().describe('ID ou nome do canal Slack'),
text: z.string().describe('Texto da mensagem'),
thread_ts: z.string().optional().describe('Timestamp do thread (opcional)'),
});
const SlackChannelSchema = z.object({
types: z.string().optional().describe('Tipos de canais: public_channel,private_channel,mpim,im'),
limit: z.number().optional().describe('Número máximo de canais (padrão: 100)'),
});
const SlackUserSchema = z.object({
limit: z.number().optional().describe('Número máximo de usuários (padrão: 100)'),
});
const SlackFileUploadSchema = z.object({
channels: z.string().describe('Canal para upload (ID ou nome)'),
file_path: z.string().describe('Caminho do arquivo local'),
title: z.string().optional().describe('Título do arquivo'),
initial_comment: z.string().optional().describe('Comentário inicial'),
});
const SlackReactionSchema = z.object({
channel: z.string().describe('ID do canal'),
timestamp: z.string().describe('Timestamp da mensagem'),
name: z.string().describe('Nome do emoji (sem :)'),
});
const SlackScheduleMessageSchema = z.object({
channel: z.string().describe('Canal de destino'),
text: z.string().describe('Texto da mensagem'),
post_at: z.number().describe('Timestamp Unix para envio'),
});
const SlackThreadReplySchema = z.object({
channel: z.string().describe('ID do canal'),
thread_ts: z.string().describe('Timestamp do thread principal'),
text: z.string().describe('Texto da resposta'),
});
const SlackUserStatusSchema = z.object({
status_text: z.string().describe('Texto do status'),
status_emoji: z.string().describe('Emoji do status (ex: :house:)'),
status_expiration: z.number().optional().describe('Timestamp de expiração'),
});
const SlackPollSchema = z.object({
channel: z.string().describe('Canal para a enquete'),
question: z.string().describe('Pergunta da enquete'),
options: z.array(z.string()).describe('Opções da enquete'),
anonymous: z.boolean().optional().describe('Enquete anônima'),
});
const SlackWebhookSchema = z.object({
url: z.string().describe('URL do webhook'),
text: z.string().describe('Texto da mensagem'),
username: z.string().optional().describe('Nome do bot'),
icon_emoji: z.string().optional().describe('Emoji do ícone'),
});
const SlackAnalyticsSchema = z.object({
channel: z.string().describe('Canal para análise'),
days: z.number().optional().describe('Dias para análise (padrão: 7)'),
});
const SlackMentionSchema = z.object({
channel: z.string().describe('Canal da mensagem'),
text: z.string().describe('Texto da mensagem'),
users: z.array(z.string()).describe('IDs dos usuários para mencionar'),
});
class SlackMCPServer {
private server: Server;
private slackClient: WebClient;
constructor() {
this.server = new Server(
{
name: 'slack-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// Configurar cliente Slack
const slackToken = process.env.SLACK_BOT_TOKEN;
if (!slackToken) {
throw new Error('SLACK_BOT_TOKEN não encontrado nas variáveis de ambiente');
}
this.slackClient = new WebClient(slackToken);
this.setupToolHandlers();
}
private setupToolHandlers() {
// Listar ferramentas disponíveis
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'send_slack_message',
description: 'Enviar uma mensagem para um canal do Slack',
inputSchema: {
type: 'object',
properties: {
channel: {
type: 'string',
description: 'ID ou nome do canal Slack (ex: #geral, C1234567890)',
},
text: {
type: 'string',
description: 'Texto da mensagem a ser enviada',
},
thread_ts: {
type: 'string',
description: 'Timestamp do thread para responder (opcional)',
},
},
required: ['channel', 'text'],
},
},
{
name: 'list_slack_channels',
description: 'Listar canais do Slack',
inputSchema: {
type: 'object',
properties: {
types: {
type: 'string',
description: 'Tipos de canais: public_channel,private_channel,mpim,im',
default: 'public_channel,private_channel',
},
limit: {
type: 'number',
description: 'Número máximo de canais',
default: 100,
},
},
required: [],
},
},
{
name: 'list_slack_users',
description: 'Listar usuários do workspace Slack',
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Número máximo de usuários',
default: 100,
},
},
required: [],
},
},
{
name: 'get_slack_channel_history',
description: 'Obter histórico de mensagens de um canal',
inputSchema: {
type: 'object',
properties: {
channel: {
type: 'string',
description: 'ID do canal Slack',
},
limit: {
type: 'number',
description: 'Número máximo de mensagens',
default: 10,
},
},
required: ['channel'],
},
},
{
name: 'upload_file_to_slack',
description: 'Fazer upload de arquivo para canal Slack',
inputSchema: {
type: 'object',
properties: {
channels: {
type: 'string',
description: 'Canal para upload (ID ou nome)',
},
file_path: {
type: 'string',
description: 'Caminho do arquivo local',
},
title: {
type: 'string',
description: 'Título do arquivo',
},
initial_comment: {
type: 'string',
description: 'Comentário inicial',
},
},
required: ['channels', 'file_path'],
},
},
{
name: 'add_slack_reaction',
description: 'Adicionar reação emoji a uma mensagem',
inputSchema: {
type: 'object',
properties: {
channel: {
type: 'string',
description: 'ID do canal',
},
timestamp: {
type: 'string',
description: 'Timestamp da mensagem',
},
name: {
type: 'string',
description: 'Nome do emoji (sem os :)',
},
},
required: ['channel', 'timestamp', 'name'],
},
},
{
name: 'schedule_slack_message',
description: 'Agendar mensagem para envio futuro',
inputSchema: {
type: 'object',
properties: {
channel: {
type: 'string',
description: 'Canal de destino',
},
text: {
type: 'string',
description: 'Texto da mensagem',
},
post_at: {
type: 'number',
description: 'Timestamp Unix para envio',
},
},
required: ['channel', 'text', 'post_at'],
},
},
{
name: 'reply_in_thread',
description: 'Responder em thread específico',
inputSchema: {
type: 'object',
properties: {
channel: {
type: 'string',
description: 'ID do canal',
},
thread_ts: {
type: 'string',
description: 'Timestamp do thread principal',
},
text: {
type: 'string',
description: 'Texto da resposta',
},
},
required: ['channel', 'thread_ts', 'text'],
},
},
{
name: 'set_user_status',
description: 'Definir status do usuário (presença)',
inputSchema: {
type: 'object',
properties: {
status_text: {
type: 'string',
description: 'Texto do status',
},
status_emoji: {
type: 'string',
description: 'Emoji do status (ex: :house:)',
},
status_expiration: {
type: 'number',
description: 'Timestamp de expiração',
},
},
required: ['status_text', 'status_emoji'],
},
},
{
name: 'search_slack_messages',
description: 'Buscar mensagens no workspace',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Termo de busca',
},
count: {
type: 'number',
description: 'Número de resultados',
default: 20,
},
sort: {
type: 'string',
description: 'Ordenação: timestamp ou score',
default: 'timestamp',
},
},
required: ['query'],
},
},
{
name: 'get_workspace_info',
description: 'Obter informações do workspace Slack',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'create_slack_reminder',
description: 'Criar lembrete no Slack',
inputSchema: {
type: 'object',
properties: {
text: {
type: 'string',
description: 'Texto do lembrete',
},
time: {
type: 'string',
description: 'Quando lembrar (ex: "in 5 minutes", "tomorrow at 9am")',
},
user: {
type: 'string',
description: 'ID do usuário (opcional, padrão: próprio usuário)',
},
},
required: ['text', 'time'],
},
},
{
name: 'create_slack_poll',
description: 'Criar enquete interativa no Slack',
inputSchema: {
type: 'object',
properties: {
channel: {
type: 'string',
description: 'Canal para a enquete',
},
question: {
type: 'string',
description: 'Pergunta da enquete',
},
options: {
type: 'array',
items: { type: 'string' },
description: 'Opções da enquete (máximo 10)',
},
anonymous: {
type: 'boolean',
description: 'Enquete anônima',
default: false,
},
},
required: ['channel', 'question', 'options'],
},
},
{
name: 'send_webhook_message',
description: 'Enviar mensagem via webhook (para integrações)',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'URL do webhook Slack',
},
text: {
type: 'string',
description: 'Texto da mensagem',
},
username: {
type: 'string',
description: 'Nome do bot',
},
icon_emoji: {
type: 'string',
description: 'Emoji do ícone (ex: :robot_face:)',
},
},
required: ['url', 'text'],
},
},
{
name: 'get_channel_analytics',
description: 'Obter analytics de um canal (atividade, engajamento)',
inputSchema: {
type: 'object',
properties: {
channel: {
type: 'string',
description: 'ID do canal',
},
days: {
type: 'number',
description: 'Dias para análise',
default: 7,
},
},
required: ['channel'],
},
},
{
name: 'mention_users',
description: 'Enviar mensagem mencionando usuários específicos',
inputSchema: {
type: 'object',
properties: {
channel: {
type: 'string',
description: 'Canal da mensagem',
},
text: {
type: 'string',
description: 'Texto da mensagem',
},
users: {
type: 'array',
items: { type: 'string' },
description: 'IDs dos usuários para mencionar',
},
},
required: ['channel', 'text', 'users'],
},
},
{
name: 'archive_old_channels',
description: 'Arquivar canais inativos automaticamente',
inputSchema: {
type: 'object',
properties: {
days_inactive: {
type: 'number',
description: 'Dias sem atividade para arquivar',
default: 30,
},
dry_run: {
type: 'boolean',
description: 'Apenas simular (não arquivar)',
default: true,
},
},
required: [],
},
},
{
name: 'bulk_invite_users',
description: 'Convidar múltiplos usuários para canal',
inputSchema: {
type: 'object',
properties: {
channel: {
type: 'string',
description: 'ID do canal',
},
users: {
type: 'array',
items: { type: 'string' },
description: 'IDs dos usuários',
},
},
required: ['channel', 'users'],
},
},
{
name: 'get_private_channels_with_user',
description: 'Listar grupos privados que o usuário participa',
inputSchema: {
type: 'object',
properties: {
user_id: {
type: 'string',
description: 'ID do usuário (opcional, padrão: bot atual)',
},
include_history: {
type: 'boolean',
description: 'Incluir última mensagem de cada grupo',
default: false,
},
},
required: [],
},
},
{
name: 'search_in_private_groups',
description: 'Buscar mensagens especificamente em grupos privados',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Termo de busca',
},
group_ids: {
type: 'array',
items: { type: 'string' },
description: 'IDs dos grupos específicos (opcional)',
},
count: {
type: 'number',
description: 'Número de resultados',
default: 20,
},
},
required: ['query'],
},
},
],
};
});
// Executar ferramentas
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'send_slack_message':
return await this.sendSlackMessage(args);
case 'list_slack_channels':
return await this.listSlackChannels(args);
case 'list_slack_users':
return await this.listSlackUsers(args);
case 'get_slack_channel_history':
return await this.getChannelHistory(args);
case 'upload_file_to_slack':
return await this.uploadFile(args);
case 'add_slack_reaction':
return await this.addReaction(args);
case 'schedule_slack_message':
return await this.scheduleMessage(args);
case 'reply_in_thread':
return await this.replyInThread(args);
case 'set_user_status':
return await this.setUserStatus(args);
case 'search_slack_messages':
return await this.searchMessages(args);
case 'get_workspace_info':
return await this.getWorkspaceInfo();
case 'create_slack_reminder':
return await this.createReminder(args);
case 'create_slack_poll':
return await this.createPoll(args);
case 'send_webhook_message':
return await this.sendWebhookMessage(args);
case 'get_channel_analytics':
return await this.getChannelAnalytics(args);
case 'mention_users':
return await this.mentionUsers(args);
case 'archive_old_channels':
return await this.archiveOldChannels(args);
case 'bulk_invite_users':
return await this.bulkInviteUsers(args);
case 'get_private_channels_with_user':
return await this.getPrivateChannelsWithUser(args);
case 'search_in_private_groups':
return await this.searchInPrivateGroups(args);
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Ferramenta desconhecida: ${name}`
);
}
} catch (error) {
if (error instanceof McpError) {
throw error;
}
throw new McpError(
ErrorCode.InternalError,
`Erro ao executar ${name}: ${error instanceof Error ? error.message : String(error)}`
);
}
});
}
private async sendSlackMessage(args: any) {
const { channel, text, thread_ts } = SlackMessageSchema.parse(args);
const result = await this.slackClient.chat.postMessage({
channel,
text,
thread_ts,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
message: 'Mensagem enviada com sucesso',
channel: result.channel,
ts: result.ts,
message_text: text,
},
null,
2
),
},
],
};
}
private async listSlackChannels(args: any) {
const { types = 'public_channel,private_channel', limit = 100 } =
SlackChannelSchema.parse(args);
const result = await this.slackClient.conversations.list({
types,
limit,
});
const channels = result.channels?.map(channel => ({
id: channel.id,
name: channel.name,
is_private: channel.is_private,
is_member: channel.is_member,
topic: channel.topic?.value,
purpose: channel.purpose?.value,
num_members: channel.num_members,
})) || [];
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
channels_count: channels.length,
channels,
},
null,
2
),
},
],
};
}
private async listSlackUsers(args: any) {
const { limit = 100 } = SlackUserSchema.parse(args);
const result = await this.slackClient.users.list({
limit,
});
const users = result.members?.map(user => ({
id: user.id,
name: user.name,
real_name: user.real_name,
display_name: user.profile?.display_name,
email: user.profile?.email,
is_bot: user.is_bot,
is_admin: user.is_admin,
is_owner: user.is_owner,
})) || [];
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
users_count: users.length,
users,
},
null,
2
),
},
],
};
}
private async getChannelHistory(args: any) {
const { channel, limit = 10 } = args;
const result = await this.slackClient.conversations.history({
channel,
limit,
});
const messages = result.messages?.map(message => ({
ts: message.ts,
user: message.user,
text: message.text,
thread_ts: message.thread_ts,
reply_count: message.reply_count,
})) || [];
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
channel,
messages_count: messages.length,
messages,
},
null,
2
),
},
],
};
}
private async uploadFile(args: any) {
const { channels, file_path, title, initial_comment } = SlackFileUploadSchema.parse(args);
try {
const fs = await import('fs');
const result = await this.slackClient.files.uploadV2({
channels,
file: fs.createReadStream(file_path),
title,
initial_comment,
}) as any;
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
message: 'Arquivo enviado com sucesso',
file: {
id: result.file?.id,
name: result.file?.name,
url: result.file?.url_private,
},
},
null,
2
),
},
],
};
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Erro ao fazer upload: ${error instanceof Error ? error.message : String(error)}`
);
}
}
private async addReaction(args: any) {
const { channel, timestamp, name } = SlackReactionSchema.parse(args);
const result = await this.slackClient.reactions.add({
channel,
timestamp,
name,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
message: `Reação :${name}: adicionada com sucesso`,
reaction: name,
timestamp,
},
null,
2
),
},
],
};
}
private async scheduleMessage(args: any) {
const { channel, text, post_at } = SlackScheduleMessageSchema.parse(args);
const result = await this.slackClient.chat.scheduleMessage({
channel,
text,
post_at,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
message: 'Mensagem agendada com sucesso',
scheduled_message_id: result.scheduled_message_id,
post_at: new Date(post_at * 1000).toISOString(),
},
null,
2
),
},
],
};
}
private async replyInThread(args: any) {
const { channel, thread_ts, text } = SlackThreadReplySchema.parse(args);
const result = await this.slackClient.chat.postMessage({
channel,
text,
thread_ts,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
message: 'Resposta enviada no thread',
thread_ts,
message_ts: result.ts,
},
null,
2
),
},
],
};
}
private async setUserStatus(args: any) {
const { status_text, status_emoji, status_expiration } = SlackUserStatusSchema.parse(args);
const result = await this.slackClient.users.profile.set({
profile: {
status_text,
status_emoji,
status_expiration: status_expiration || 0,
},
});
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
message: 'Status do usuário atualizado',
status: {
text: status_text,
emoji: status_emoji,
expiration: status_expiration ? new Date(status_expiration * 1000).toISOString() : null,
},
},
null,
2
),
},
],
};
}
private async searchMessages(args: any) {
const { query, count = 20, sort = 'timestamp' } = args;
const result = await this.slackClient.search.messages({
query,
count,
sort,
});
const matches = result.messages?.matches?.map(match => ({
text: match.text,
user: match.username,
channel: match.channel?.name,
ts: match.ts,
permalink: match.permalink,
})) || [];
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
query,
total_matches: result.messages?.total,
matches_count: matches.length,
matches,
},
null,
2
),
},
],
};
}
private async getWorkspaceInfo() {
const teamInfo = await this.slackClient.team.info();
const authTest = await this.slackClient.auth.test();
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
workspace: {
id: teamInfo.team?.id,
name: teamInfo.team?.name,
domain: teamInfo.team?.domain,
email_domain: teamInfo.team?.email_domain,
icon: teamInfo.team?.icon,
},
bot: {
user_id: authTest.user_id,
bot_id: authTest.bot_id,
team_id: authTest.team_id,
},
},
null,
2
),
},
],
};
}
private async createReminder(args: any) {
const { text, time, user } = args;
const result = await this.slackClient.reminders.add({
text,
time,
user,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
message: 'Lembrete criado com sucesso',
reminder: {
id: result.reminder?.id,
text: result.reminder?.text,
time: result.reminder?.time,
user: result.reminder?.user,
},
},
null,
2
),
},
],
};
}
private async createPoll(args: any) {
const { channel, question, options, anonymous = false } = SlackPollSchema.parse(args);
// Criar enquete usando Block Kit
const blocks: any[] = [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*${question}*${anonymous ? ' (Enquete Anônima)' : ''}`,
},
},
{
type: 'divider',
},
];
// Adicionar botões para cada opção
const actions = options.slice(0, 10).map((option, index) => ({
type: 'button',
text: {
type: 'plain_text',
text: option,
},
action_id: `poll_option_${index}`,
value: `${index}`,
}));
blocks.push({
type: 'actions',
elements: actions,
});
const result = await this.slackClient.chat.postMessage({
channel,
text: question,
blocks,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
message: 'Enquete criada com sucesso',
poll: {
question,
options,
message_ts: result.ts,
anonymous,
},
},
null,
2
),
},
],
};
}
private async sendWebhookMessage(args: any) {
const { url, text, username, icon_emoji } = SlackWebhookSchema.parse(args);
try {
const payload = {
text,
username,
icon_emoji,
};
const response = await axios.post(url, payload);
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
message: 'Mensagem webhook enviada',
status: response.status,
},
null,
2
),
},
],
};
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Erro ao enviar webhook: ${error instanceof Error ? error.message : String(error)}`
);
}
}
private async getChannelAnalytics(args: any) {
const { channel, days = 7 } = SlackAnalyticsSchema.parse(args);
const oldest = Math.floor((Date.now() - days * 24 * 60 * 60 * 1000) / 1000);
const result = await this.slackClient.conversations.history({
channel,
oldest: oldest.toString(),
limit: 1000,
});
const messages = result.messages || [];
const userStats: Record<string, number> = {};
const hourStats: Record<string, number> = {};
let totalReactions = 0;
messages.forEach(message => {
if (message.user) {
userStats[message.user] = (userStats[message.user] || 0) + 1;
}
if (message.ts) {
const hour = new Date(parseFloat(message.ts) * 1000).getHours();
hourStats[hour] = (hourStats[hour] || 0) + 1;
}
if (message.reactions) {
totalReactions += message.reactions.reduce((sum, reaction) => sum + (reaction.count || 0), 0);
}
});
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
analytics: {
period_days: days,
total_messages: messages.length,
total_reactions: totalReactions,
most_active_users: Object.entries(userStats)
.sort(([,a], [,b]) => b - a)
.slice(0, 5),
activity_by_hour: hourStats,
engagement_rate: messages.length > 0 ? totalReactions / messages.length : 0,
},
},
null,
2
),
},
],
};
}
private async mentionUsers(args: any) {
const { channel, text, users } = SlackMentionSchema.parse(args);
const mentions = users.map(user => `<@${user}>`).join(' ');
const messageText = `${mentions} ${text}`;
const result = await this.slackClient.chat.postMessage({
channel,
text: messageText,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
message: 'Mensagem com menções enviada',
mentioned_users: users.length,
message_ts: result.ts,
},
null,
2
),
},
],
};
}
private async archiveOldChannels(args: any) {
const { days_inactive = 30, dry_run = true } = args;
const cutoffDate = Math.floor((Date.now() - days_inactive * 24 * 60 * 60 * 1000) / 1000);
const channelsResult = await this.slackClient.conversations.list({
types: 'public_channel,private_channel',
limit: 100,
});
const inactiveChannels = [];
for (const channel of channelsResult.channels || []) {
if (!channel.id) continue;
const history = await this.slackClient.conversations.history({
channel: channel.id,
limit: 1,
});
const lastMessage = history.messages?.[0];
if (!lastMessage?.ts || parseFloat(lastMessage.ts) < cutoffDate) {
inactiveChannels.push({
id: channel.id,
name: channel.name,
last_activity: lastMessage?.ts ? new Date(parseFloat(lastMessage.ts) * 1000).toISOString() : 'Never',
});
if (!dry_run && channel.id) {
await this.slackClient.conversations.archive({
channel: channel.id,
});
}
}
}
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
dry_run,
inactive_channels: inactiveChannels.length,
channels: inactiveChannels,
message: dry_run ? 'Simulação concluída' : `${inactiveChannels.length} canais arquivados`,
},
null,
2
),
},
],
};
}
private async bulkInviteUsers(args: any) {
const { channel, users } = args;
const results = [];
for (const user of users) {
try {
await this.slackClient.conversations.invite({
channel,
users: user,
});
results.push({ user, success: true });
} catch (error) {
results.push({
user,
success: false,
error: error instanceof Error ? error.message : String(error)
});
}
}
const successful = results.filter(r => r.success).length;
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
invited_users: successful,
total_users: users.length,
results,
},
null,
2
),
},
],
};
}
private async getPrivateChannelsWithUser(args: any) {
const { user_id, include_history = false } = args;
// Listar todos os grupos privados que o bot tem acesso
const result = await this.slackClient.conversations.list({
types: 'private_channel,mpim',
limit: 200,
});
const privateChannels = [];
for (const channel of result.channels || []) {
if (!channel.id) continue;
try {
// Verificar se o usuário está no canal (se especificado)
if (user_id) {
const members = await this.slackClient.conversations.members({
channel: channel.id,
});
if (!members.members?.includes(user_id)) {
continue; // Usuário não está neste canal
}
}
const channelInfo: any = {
id: channel.id,
name: channel.name,
is_group: channel.is_group,
is_mpim: channel.is_mpim,
num_members: channel.num_members,
topic: channel.topic?.value,
purpose: channel.purpose?.value,
};
// Incluir última mensagem se solicitado
if (include_history) {
try {
const history = await this.slackClient.conversations.history({
channel: channel.id,
limit: 1,
});
const lastMessage = history.messages?.[0];
if (lastMessage) {
channelInfo.last_message = {
text: lastMessage.text,
user: lastMessage.user,
ts: lastMessage.ts,
date: new Date(parseFloat(lastMessage.ts || '0') * 1000).toISOString(),
};
}
} catch (historyError) {
channelInfo.last_message = null;
}
}
privateChannels.push(channelInfo);
} catch (error) {
console.error(`Erro ao processar canal ${channel.id}:`, error);
}
}
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
user_id: user_id || 'bot_user',
private_channels_count: privateChannels.length,
private_channels: privateChannels,
},
null,
2
),
},
],
};
}
private async searchInPrivateGroups(args: any) {
const { query, group_ids, count = 20 } = args;
try {
// Se grupos específicos foram fornecidos, buscar apenas neles
if (group_ids && group_ids.length > 0) {
const results = [];
for (const groupId of group_ids) {
try {
const searchQuery = `in:${groupId} ${query}`;
const result = await this.slackClient.search.messages({
query: searchQuery,
count: Math.floor(count / group_ids.length),
});
const matches = result.messages?.matches?.map(match => ({
text: match.text,
user: match.username,
channel: match.channel?.name,
channel_id: groupId,
ts: match.ts,
permalink: match.permalink,
})) || [];
results.push(...matches);
} catch (error) {
console.error(`Erro ao buscar no grupo ${groupId}:`, error);
}
}
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
query,
searched_groups: group_ids,
matches_count: results.length,
matches: results.slice(0, count),
},
null,
2
),
},
],
};
}
// Busca geral limitada a canais privados
const searchQuery = `${query} in:private`;
const result = await this.slackClient.search.messages({
query: searchQuery,
count,
});
const matches = result.messages?.matches?.map(match => ({
text: match.text,
user: match.username,
channel: match.channel?.name,
ts: match.ts,
permalink: match.permalink,
is_private: true,
})) || [];
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
query,
search_scope: 'private_channels',
total_matches: result.messages?.total,
matches_count: matches.length,
matches,
},
null,
2
),
},
],
};
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Erro ao buscar em grupos privados: ${error instanceof Error ? error.message : String(error)}`
);
}
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Slack MCP Server iniciado');
}
}
const server = new SlackMCPServer();
server.run().catch(console.error);