Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,13 @@ model Message {
authorId String @map("author_id")
authorName String @map("author_name")
content String
messageType String @default("chat") @map("message_type") // chat, transcription
timestamp DateTime
projectId String? @map("project_id")
createdAt DateTime @default(now()) @map("created_at")
messageType String @default("chat") @map("message_type") // chat, transcription
timestamp DateTime
projectId String? @map("project_id")
metadata Json? // Original message metadata
mcpRetrieved Boolean @default(false) @map("mcp_retrieved") // Retrieved via MCP
mcpMetadata Json? @map("mcp_metadata") // MCP-specific metadata
createdAt DateTime @default(now()) @map("created_at")

project Project? @relation(fields: [projectId], references: [id], onDelete: SetNull)
entities Entity[]
Expand All @@ -67,14 +70,18 @@ model Message {

model AudioRecording {
id String @id @default(uuid())
source String // teams, slack
source String // teams, slack, slack-mcp, teams-mcp
sourceId String @map("source_id")
meetingTitle String? @map("meeting_title")
fileUrl String? @map("file_url")
storagePath String? @map("storage_path")
s3Key String? @map("s3_key") // S3 storage key
durationSeconds Int? @map("duration_seconds")
duration Int? // Duration in seconds (alias for durationSeconds)
transcriptionStatus String @default("pending") @map("transcription_status") // pending, processing, completed, failed
timestamp DateTime
timestamp DateTime?
mcpRetrieved Boolean @default(false) @map("mcp_retrieved") // Retrieved via MCP
mcpMetadata Json? @map("mcp_metadata") // MCP-specific metadata
createdAt DateTime @default(now()) @map("created_at")

transcriptions Transcription[]
Expand Down
2 changes: 2 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { WebhooksModule } from './modules/webhooks/webhooks.module';
import { AdminModule } from './modules/admin/admin.module';
import { AnalyticsModule } from './modules/analytics/analytics.module';
import { MCPModule } from './modules/mcp/mcp.module';
import { McpSlackModule } from './modules/mcp-slack/mcp-slack.module';

@Module({
imports: [
Expand Down Expand Up @@ -55,6 +56,7 @@ import { MCPModule } from './modules/mcp/mcp.module';
AdminModule,
AnalyticsModule,
MCPModule,
McpSlackModule,
],
controllers: [AppController],
providers: [AppService],
Expand Down
309 changes: 309 additions & 0 deletions backend/src/modules/mcp-slack/controllers/mcp-slack.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
import {
Controller,
Get,
Post,
Body,
Query,
Param,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { McpSlackService } from '../services/mcp-slack.service';
import {
SyncChannelDto,
GetMessagesDto,
ProcessAudioDto,
SyncAudioDto,
} from '../dto/slack-mcp.dto';

/**
* Slack MCP Controller
* REST API endpoints for Slack MCP integration
*/
@ApiTags('slack-mcp')
@Controller('slack-mcp')
export class McpSlackController {
constructor(private readonly mcpSlackService: McpSlackService) {}

/**
* Check if Slack MCP is available
*/
@Get('status')
@ApiOperation({ summary: 'Check Slack MCP status' })
@ApiResponse({ status: 200, description: 'MCP status retrieved' })
async getStatus() {
const isAvailable = this.mcpSlackService.isAvailable();

return {
available: isAvailable,
server: 'slack',
timestamp: new Date().toISOString(),
};
}

/**
* List all Slack channels
*/
@Get('channels')
@ApiOperation({ summary: 'List all Slack channels' })
@ApiResponse({ status: 200, description: 'Channels retrieved successfully' })
async listChannels() {
try {
const channels = await this.mcpSlackService.listChannels();

return {
success: true,
count: channels.length,
channels,
};
} catch (error) {
throw new HttpException(
{
success: false,
message: 'Failed to list channels',
error: error instanceof Error ? error.message : 'Unknown error',
},
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}

/**
* Get channel info
*/
@Get('channels/:channelId')
@ApiOperation({ summary: 'Get Slack channel information' })
@ApiResponse({ status: 200, description: 'Channel info retrieved' })
async getChannelInfo(@Param('channelId') channelId: string) {
try {
const channel = await this.mcpSlackService.getChannelInfo(channelId);

return {
success: true,
channel,
};
} catch (error) {
throw new HttpException(
{
success: false,
message: `Failed to get channel info for ${channelId}`,
error: error instanceof Error ? error.message : 'Unknown error',
},
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}

/**
* Get channel messages
*/
@Get('messages')
@ApiOperation({ summary: 'Get messages from a Slack channel' })
@ApiResponse({ status: 200, description: 'Messages retrieved successfully' })
async getMessages(@Query() query: GetMessagesDto) {
try {
const messages = await this.mcpSlackService.getChannelMessages(query.channelId, {
limit: query.limit,
oldest: query.oldest,
latest: query.latest,
});

return {
success: true,
channelId: query.channelId,
count: messages.length,
messages,
};
} catch (error) {
throw new HttpException(
{
success: false,
message: 'Failed to get messages',
error: error instanceof Error ? error.message : 'Unknown error',
},
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}

/**
* Sync channel messages to database
*/
@Post('sync')
@ApiOperation({ summary: 'Sync Slack channel messages to database' })
@ApiResponse({ status: 200, description: 'Sync completed successfully' })
async syncChannel(@Body() dto: SyncChannelDto) {
try {
const result = await this.mcpSlackService.syncChannelHistory(dto.channelId, {
limit: dto.limit,
oldest: dto.oldest,
latest: dto.latest,
});

return {
success: true,
result,
};
} catch (error) {
throw new HttpException(
{
success: false,
message: 'Sync failed',
error: error instanceof Error ? error.message : 'Unknown error',
},
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}

/**
* Get user info
*/
@Get('users/:userId')
@ApiOperation({ summary: 'Get Slack user information' })
@ApiResponse({ status: 200, description: 'User info retrieved' })
async getUserInfo(@Param('userId') userId: string) {
try {
const user = await this.mcpSlackService.getUserInfo(userId);

return {
success: true,
user,
};
} catch (error) {
throw new HttpException(
{
success: false,
message: `Failed to get user info for ${userId}`,
error: error instanceof Error ? error.message : 'Unknown error',
},
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}

/**
* Get audio recordings from a channel
*/
@Get('audio/:channelId')
@ApiOperation({ summary: 'Get audio recordings from a Slack channel' })
@ApiResponse({ status: 200, description: 'Audio recordings retrieved' })
async getAudioRecordings(@Param('channelId') channelId: string, @Query('since') since?: string) {
try {
const sinceDate = since ? new Date(since) : undefined;
const recordings = await this.mcpSlackService.getAudioRecordings(channelId, sinceDate);

return {
success: true,
channelId,
count: recordings.length,
recordings,
};
} catch (error) {
throw new HttpException(
{
success: false,
message: 'Failed to get audio recordings',
error: error instanceof Error ? error.message : 'Unknown error',
},
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}

/**
* Process a specific audio file
*/
@Post('audio/process')
@ApiOperation({ summary: 'Process a Slack audio file' })
@ApiResponse({ status: 200, description: 'Audio processing queued' })
async processAudio(@Body() dto: ProcessAudioDto) {
try {
await this.mcpSlackService.processAudioRecording(dto.fileId);

return {
success: true,
message: `Audio file ${dto.fileId} queued for processing`,
fileId: dto.fileId,
};
} catch (error) {
throw new HttpException(
{
success: false,
message: 'Failed to process audio',
error: error instanceof Error ? error.message : 'Unknown error',
},
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}

/**
* Sync audio files from a channel
*/
@Post('audio/sync')
@ApiOperation({ summary: 'Sync audio files from a Slack channel' })
@ApiResponse({ status: 200, description: 'Audio sync started' })
async syncAudio(@Body() dto: SyncAudioDto) {
try {
const sinceDate = dto.since ? new Date(dto.since) : undefined;
const recordings = await this.mcpSlackService.getAudioRecordings(dto.channelId, sinceDate);

// Process each recording (limit to dto.limit if specified)
const toProcess = dto.limit ? recordings.slice(0, dto.limit) : recordings;

for (const recording of toProcess) {
try {
await this.mcpSlackService.processAudioRecording(recording.fileId);
} catch (error) {
// Continue processing other files even if one fails
continue;
}
}

return {
success: true,
message: 'Audio sync started',
channelId: dto.channelId,
totalFound: recordings.length,
queued: toProcess.length,
};
} catch (error) {
throw new HttpException(
{
success: false,
message: 'Audio sync failed',
error: error instanceof Error ? error.message : 'Unknown error',
},
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}

/**
* Get file info
*/
@Get('files/:fileId')
@ApiOperation({ summary: 'Get Slack file information' })
@ApiResponse({ status: 200, description: 'File info retrieved' })
async getFileInfo(@Param('fileId') fileId: string) {
try {
const file = await this.mcpSlackService.getFileInfo(fileId);

return {
success: true,
file,
};
} catch (error) {
throw new HttpException(
{
success: false,
message: `Failed to get file info for ${fileId}`,
error: error instanceof Error ? error.message : 'Unknown error',
},
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
Loading