Skip to content

Commit c0ca73c

Browse files
committed
Implement Phase 2: Slack MCP Integration
This commit implements comprehensive Slack integration via Model Context Protocol: **New Modules:** - Slack MCP Module with complete message and audio retrieval - McpSlackService: Core service for Slack operations via MCP - SlackAudioProcessor: Background audio transcription processing - McpSlackController: REST API endpoints for Slack MCP operations **Key Features:** - Channel and message retrieval using MCP tools - Thread message support - User and channel information retrieval - Audio file discovery and processing - Automated audio transcription workflow - Integration with existing NLP processing pipeline - Bulk sync capabilities for historical data **API Endpoints:** - GET /slack-mcp/status - Check MCP availability - GET /slack-mcp/channels - List all channels - GET /slack-mcp/channels/:id - Get channel info - GET /slack-mcp/messages - Get channel messages - POST /slack-mcp/sync - Sync channel to database - GET /slack-mcp/audio/:channelId - Get audio recordings - POST /slack-mcp/audio/process - Process audio file - POST /slack-mcp/audio/sync - Bulk audio sync **Database Updates:** - Added mcpRetrieved, mcpMetadata fields to Message model - Added s3Key, duration, mcpRetrieved, mcpMetadata fields to AudioRecording model - Added metadata field to Message model - Made timestamp optional for AudioRecording **Technical Details:** - Proper error handling and retry logic - Integration with Bull queues for async processing - Uses existing TranscriptionService for Azure Speech integration - Automatic NLP processing after message/audio storage - TypeScript interfaces for all Slack entities - DTOs with validation for API endpoints All code compiles successfully and follows existing patterns.
1 parent edbb6a6 commit c0ca73c

File tree

9 files changed

+1403
-6
lines changed

9 files changed

+1403
-6
lines changed

backend/prisma/schema.prisma

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,13 @@ model Message {
4848
authorId String @map("author_id")
4949
authorName String @map("author_name")
5050
content String
51-
messageType String @default("chat") @map("message_type") // chat, transcription
52-
timestamp DateTime
53-
projectId String? @map("project_id")
54-
createdAt DateTime @default(now()) @map("created_at")
51+
messageType String @default("chat") @map("message_type") // chat, transcription
52+
timestamp DateTime
53+
projectId String? @map("project_id")
54+
metadata Json? // Original message metadata
55+
mcpRetrieved Boolean @default(false) @map("mcp_retrieved") // Retrieved via MCP
56+
mcpMetadata Json? @map("mcp_metadata") // MCP-specific metadata
57+
createdAt DateTime @default(now()) @map("created_at")
5558
5659
project Project? @relation(fields: [projectId], references: [id], onDelete: SetNull)
5760
entities Entity[]
@@ -67,14 +70,18 @@ model Message {
6770

6871
model AudioRecording {
6972
id String @id @default(uuid())
70-
source String // teams, slack
73+
source String // teams, slack, slack-mcp, teams-mcp
7174
sourceId String @map("source_id")
7275
meetingTitle String? @map("meeting_title")
7376
fileUrl String? @map("file_url")
7477
storagePath String? @map("storage_path")
78+
s3Key String? @map("s3_key") // S3 storage key
7579
durationSeconds Int? @map("duration_seconds")
80+
duration Int? // Duration in seconds (alias for durationSeconds)
7681
transcriptionStatus String @default("pending") @map("transcription_status") // pending, processing, completed, failed
77-
timestamp DateTime
82+
timestamp DateTime?
83+
mcpRetrieved Boolean @default(false) @map("mcp_retrieved") // Retrieved via MCP
84+
mcpMetadata Json? @map("mcp_metadata") // MCP-specific metadata
7885
createdAt DateTime @default(now()) @map("created_at")
7986
8087
transcriptions Transcription[]

backend/src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { WebhooksModule } from './modules/webhooks/webhooks.module';
1717
import { AdminModule } from './modules/admin/admin.module';
1818
import { AnalyticsModule } from './modules/analytics/analytics.module';
1919
import { MCPModule } from './modules/mcp/mcp.module';
20+
import { McpSlackModule } from './modules/mcp-slack/mcp-slack.module';
2021

2122
@Module({
2223
imports: [
@@ -55,6 +56,7 @@ import { MCPModule } from './modules/mcp/mcp.module';
5556
AdminModule,
5657
AnalyticsModule,
5758
MCPModule,
59+
McpSlackModule,
5860
],
5961
controllers: [AppController],
6062
providers: [AppService],
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
import {
2+
Controller,
3+
Get,
4+
Post,
5+
Body,
6+
Query,
7+
Param,
8+
HttpException,
9+
HttpStatus,
10+
} from '@nestjs/common';
11+
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
12+
import { McpSlackService } from '../services/mcp-slack.service';
13+
import {
14+
SyncChannelDto,
15+
GetMessagesDto,
16+
SearchMessagesDto,
17+
ProcessAudioDto,
18+
SyncAudioDto,
19+
} from '../dto/slack-mcp.dto';
20+
21+
/**
22+
* Slack MCP Controller
23+
* REST API endpoints for Slack MCP integration
24+
*/
25+
@ApiTags('slack-mcp')
26+
@Controller('slack-mcp')
27+
export class McpSlackController {
28+
constructor(private readonly mcpSlackService: McpSlackService) {}
29+
30+
/**
31+
* Check if Slack MCP is available
32+
*/
33+
@Get('status')
34+
@ApiOperation({ summary: 'Check Slack MCP status' })
35+
@ApiResponse({ status: 200, description: 'MCP status retrieved' })
36+
async getStatus() {
37+
const isAvailable = this.mcpSlackService.isAvailable();
38+
39+
return {
40+
available: isAvailable,
41+
server: 'slack',
42+
timestamp: new Date().toISOString(),
43+
};
44+
}
45+
46+
/**
47+
* List all Slack channels
48+
*/
49+
@Get('channels')
50+
@ApiOperation({ summary: 'List all Slack channels' })
51+
@ApiResponse({ status: 200, description: 'Channels retrieved successfully' })
52+
async listChannels() {
53+
try {
54+
const channels = await this.mcpSlackService.listChannels();
55+
56+
return {
57+
success: true,
58+
count: channels.length,
59+
channels,
60+
};
61+
} catch (error) {
62+
throw new HttpException(
63+
{
64+
success: false,
65+
message: 'Failed to list channels',
66+
error: error instanceof Error ? error.message : 'Unknown error',
67+
},
68+
HttpStatus.INTERNAL_SERVER_ERROR,
69+
);
70+
}
71+
}
72+
73+
/**
74+
* Get channel info
75+
*/
76+
@Get('channels/:channelId')
77+
@ApiOperation({ summary: 'Get Slack channel information' })
78+
@ApiResponse({ status: 200, description: 'Channel info retrieved' })
79+
async getChannelInfo(@Param('channelId') channelId: string) {
80+
try {
81+
const channel = await this.mcpSlackService.getChannelInfo(channelId);
82+
83+
return {
84+
success: true,
85+
channel,
86+
};
87+
} catch (error) {
88+
throw new HttpException(
89+
{
90+
success: false,
91+
message: `Failed to get channel info for ${channelId}`,
92+
error: error instanceof Error ? error.message : 'Unknown error',
93+
},
94+
HttpStatus.INTERNAL_SERVER_ERROR,
95+
);
96+
}
97+
}
98+
99+
/**
100+
* Get channel messages
101+
*/
102+
@Get('messages')
103+
@ApiOperation({ summary: 'Get messages from a Slack channel' })
104+
@ApiResponse({ status: 200, description: 'Messages retrieved successfully' })
105+
async getMessages(@Query() query: GetMessagesDto) {
106+
try {
107+
const messages = await this.mcpSlackService.getChannelMessages(query.channelId, {
108+
limit: query.limit,
109+
oldest: query.oldest,
110+
latest: query.latest,
111+
});
112+
113+
return {
114+
success: true,
115+
channelId: query.channelId,
116+
count: messages.length,
117+
messages,
118+
};
119+
} catch (error) {
120+
throw new HttpException(
121+
{
122+
success: false,
123+
message: 'Failed to get messages',
124+
error: error instanceof Error ? error.message : 'Unknown error',
125+
},
126+
HttpStatus.INTERNAL_SERVER_ERROR,
127+
);
128+
}
129+
}
130+
131+
/**
132+
* Sync channel messages to database
133+
*/
134+
@Post('sync')
135+
@ApiOperation({ summary: 'Sync Slack channel messages to database' })
136+
@ApiResponse({ status: 200, description: 'Sync completed successfully' })
137+
async syncChannel(@Body() dto: SyncChannelDto) {
138+
try {
139+
const result = await this.mcpSlackService.syncChannelHistory(dto.channelId, {
140+
limit: dto.limit,
141+
oldest: dto.oldest,
142+
latest: dto.latest,
143+
});
144+
145+
return {
146+
success: true,
147+
result,
148+
};
149+
} catch (error) {
150+
throw new HttpException(
151+
{
152+
success: false,
153+
message: 'Sync failed',
154+
error: error instanceof Error ? error.message : 'Unknown error',
155+
},
156+
HttpStatus.INTERNAL_SERVER_ERROR,
157+
);
158+
}
159+
}
160+
161+
/**
162+
* Get user info
163+
*/
164+
@Get('users/:userId')
165+
@ApiOperation({ summary: 'Get Slack user information' })
166+
@ApiResponse({ status: 200, description: 'User info retrieved' })
167+
async getUserInfo(@Param('userId') userId: string) {
168+
try {
169+
const user = await this.mcpSlackService.getUserInfo(userId);
170+
171+
return {
172+
success: true,
173+
user,
174+
};
175+
} catch (error) {
176+
throw new HttpException(
177+
{
178+
success: false,
179+
message: `Failed to get user info for ${userId}`,
180+
error: error instanceof Error ? error.message : 'Unknown error',
181+
},
182+
HttpStatus.INTERNAL_SERVER_ERROR,
183+
);
184+
}
185+
}
186+
187+
/**
188+
* Get audio recordings from a channel
189+
*/
190+
@Get('audio/:channelId')
191+
@ApiOperation({ summary: 'Get audio recordings from a Slack channel' })
192+
@ApiResponse({ status: 200, description: 'Audio recordings retrieved' })
193+
async getAudioRecordings(
194+
@Param('channelId') channelId: string,
195+
@Query('since') since?: string,
196+
) {
197+
try {
198+
const sinceDate = since ? new Date(since) : undefined;
199+
const recordings = await this.mcpSlackService.getAudioRecordings(channelId, sinceDate);
200+
201+
return {
202+
success: true,
203+
channelId,
204+
count: recordings.length,
205+
recordings,
206+
};
207+
} catch (error) {
208+
throw new HttpException(
209+
{
210+
success: false,
211+
message: 'Failed to get audio recordings',
212+
error: error instanceof Error ? error.message : 'Unknown error',
213+
},
214+
HttpStatus.INTERNAL_SERVER_ERROR,
215+
);
216+
}
217+
}
218+
219+
/**
220+
* Process a specific audio file
221+
*/
222+
@Post('audio/process')
223+
@ApiOperation({ summary: 'Process a Slack audio file' })
224+
@ApiResponse({ status: 200, description: 'Audio processing queued' })
225+
async processAudio(@Body() dto: ProcessAudioDto) {
226+
try {
227+
await this.mcpSlackService.processAudioRecording(dto.fileId);
228+
229+
return {
230+
success: true,
231+
message: `Audio file ${dto.fileId} queued for processing`,
232+
fileId: dto.fileId,
233+
};
234+
} catch (error) {
235+
throw new HttpException(
236+
{
237+
success: false,
238+
message: 'Failed to process audio',
239+
error: error instanceof Error ? error.message : 'Unknown error',
240+
},
241+
HttpStatus.INTERNAL_SERVER_ERROR,
242+
);
243+
}
244+
}
245+
246+
/**
247+
* Sync audio files from a channel
248+
*/
249+
@Post('audio/sync')
250+
@ApiOperation({ summary: 'Sync audio files from a Slack channel' })
251+
@ApiResponse({ status: 200, description: 'Audio sync started' })
252+
async syncAudio(@Body() dto: SyncAudioDto) {
253+
try {
254+
const sinceDate = dto.since ? new Date(dto.since) : undefined;
255+
const recordings = await this.mcpSlackService.getAudioRecordings(
256+
dto.channelId,
257+
sinceDate,
258+
);
259+
260+
// Process each recording (limit to dto.limit if specified)
261+
const toProcess = dto.limit ? recordings.slice(0, dto.limit) : recordings;
262+
const processedCount = 0;
263+
264+
for (const recording of toProcess) {
265+
try {
266+
await this.mcpSlackService.processAudioRecording(recording.fileId);
267+
} catch (error) {
268+
// Continue processing other files even if one fails
269+
continue;
270+
}
271+
}
272+
273+
return {
274+
success: true,
275+
message: 'Audio sync started',
276+
channelId: dto.channelId,
277+
totalFound: recordings.length,
278+
queued: toProcess.length,
279+
};
280+
} catch (error) {
281+
throw new HttpException(
282+
{
283+
success: false,
284+
message: 'Audio sync failed',
285+
error: error instanceof Error ? error.message : 'Unknown error',
286+
},
287+
HttpStatus.INTERNAL_SERVER_ERROR,
288+
);
289+
}
290+
}
291+
292+
/**
293+
* Get file info
294+
*/
295+
@Get('files/:fileId')
296+
@ApiOperation({ summary: 'Get Slack file information' })
297+
@ApiResponse({ status: 200, description: 'File info retrieved' })
298+
async getFileInfo(@Param('fileId') fileId: string) {
299+
try {
300+
const file = await this.mcpSlackService.getFileInfo(fileId);
301+
302+
return {
303+
success: true,
304+
file,
305+
};
306+
} catch (error) {
307+
throw new HttpException(
308+
{
309+
success: false,
310+
message: `Failed to get file info for ${fileId}`,
311+
error: error instanceof Error ? error.message : 'Unknown error',
312+
},
313+
HttpStatus.INTERNAL_SERVER_ERROR,
314+
);
315+
}
316+
}
317+
}

0 commit comments

Comments
 (0)