Skip to main content
Glama

Mattermost MCP Server

by pvev
index.ts12.4 kB
import { MattermostClient } from '../client.js'; import { MonitoringConfig } from '../config.js'; import { Channel, Post, User, UserProfile } from '../types.js'; import { findRelevantPosts, createNotificationMessage } from './analyzer.js'; import { Scheduler } from './scheduler.js'; /** * TopicMonitor class for monitoring channels for topics of interest */ export class TopicMonitor { private client: MattermostClient; private config: MonitoringConfig; private scheduler: Scheduler; private channelCache: Map<string, string> = new Map(); // Map of channel names to IDs private currentUserId: string | null = null; private currentUsername: string | null = null; private directMessageChannelId: string | null = null; /** * Creates a new topic monitor * @param client Mattermost client * @param config Monitoring configuration */ constructor(client: MattermostClient, config: MonitoringConfig) { this.client = client; this.config = config; this.scheduler = new Scheduler(config, this.monitorChannels.bind(this)); } /** * Initializes the monitor by fetching necessary user information */ async initialize(): Promise<void> { // If userId is not provided, fetch the current user's information if (!this.config.userId) { await this.fetchCurrentUser(); } else { this.currentUserId = this.config.userId; } // If notificationChannelId is not provided, create or find a direct message channel if (!this.config.notificationChannelId) { await this.createDirectMessageChannel(); } } /** * Fetches the current user's information */ private async fetchCurrentUser(): Promise<void> { try { // Try to get the user ID from the response try { // Get the first page of users const response = await this.client.getUsers(100, 0); // If response is an array (not the expected UsersResponse format) if (Array.isArray(response)) { // Find a non-bot user (preferably sysadmin) for (const user of response) { if (user.username === 'sysadmin') { this.currentUserId = user.id; this.currentUsername = user.username; console.error(`Found sysadmin user: ${user.username} (${user.id})`); return; } } // If sysadmin not found, use any non-bot user for (const user of response) { if (!user.is_bot) { this.currentUserId = user.id; this.currentUsername = user.username; console.error(`Found user: ${user.username} (${user.id})`); return; } } // If no non-bot user found, use any user if (response.length > 0) { this.currentUserId = response[0].id; this.currentUsername = response[0].username; console.error(`Using first available user: ${response[0].username} (${response[0].id})`); return; } } else if (response && response.users && Array.isArray(response.users)) { // Standard format for (const user of response.users) { if (user.username === 'sysadmin') { this.currentUserId = user.id; this.currentUsername = user.username; console.error(`Found sysadmin user: ${user.username} (${user.id})`); return; } } for (const user of response.users) { if (!user.is_bot) { this.currentUserId = user.id; this.currentUsername = user.username; console.error(`Found user: ${user.username} (${user.id})`); return; } } } else { console.error('Response format unexpected:', response); } } catch (innerError) { console.error('Error getting users:', innerError); } // Fallback: Use the town-square channel's first post author try { // Get the town-square channel const channelsResponse = await this.client.getChannels(100, 0); let townSquareId = null; if (channelsResponse && channelsResponse.channels && Array.isArray(channelsResponse.channels)) { for (const channel of channelsResponse.channels) { if (channel.name === 'town-square') { townSquareId = channel.id; break; } } } if (townSquareId) { // Get posts from town-square const postsResponse = await this.client.getPostsForChannel(townSquareId, 1, 0); if (postsResponse && postsResponse.posts) { // Get the first post's user ID const posts = Object.values(postsResponse.posts); if (posts.length > 0) { this.currentUserId = posts[0].user_id; // Get the username for this user ID try { const userProfile = await this.client.getUserProfile(this.currentUserId); this.currentUsername = userProfile.username; console.error(`Found username for post author: ${this.currentUsername}`); } catch (profileError) { console.error('Error getting user profile:', profileError); this.currentUsername = 'user'; } console.error(`Using fallback user ID from post: ${this.currentUserId}`); return; } } } } catch (innerError) { console.error('Error using fallback method:', innerError); } // Final fallback: Use hardcoded values this.currentUserId = "system"; this.currentUsername = "system"; console.error(`Using hardcoded user ID: ${this.currentUserId}`); } catch (error) { console.error('Error fetching current user:', error); throw error; } } /** * Creates or finds a direct message channel for the current user */ private async createDirectMessageChannel(): Promise<void> { try { if (!this.currentUserId) { throw new Error('Current user ID is not set'); } // Try to create a direct message channel with the user try { // First, try to find an existing DM channel const response = await this.client.getChannels(100, 0); if (response && response.channels && Array.isArray(response.channels)) { // Look for a direct message channel for (const channel of response.channels) { // Direct message channels have type 'D' if (channel.type === 'D') { this.directMessageChannelId = channel.id; console.error(`Found direct message channel: ${channel.id}`); return; } } } // If we couldn't find a DM channel, try to create one console.error('Could not find a direct message channel, attempting to create one...'); try { // Create a direct message channel with the current user const dmChannel = await this.client.createDirectMessageChannel(this.currentUserId); this.directMessageChannelId = dmChannel.id; console.error(`Created direct message channel: ${dmChannel.id}`); return; } catch (createError) { console.error('Error creating direct message channel:', createError); } // If creating a DM channel fails, use town-square as a fallback for (const channel of response.channels) { if (channel.name === 'town-square') { this.directMessageChannelId = channel.id; console.error(`Using town-square as fallback notification channel: ${channel.id}`); return; } } } catch (innerError) { console.error('Error finding/creating DM channel:', innerError); } throw new Error('Could not find a suitable notification channel'); } catch (error) { console.error('Error creating direct message channel:', error); throw error; } } /** * Starts the monitoring process */ async start(): Promise<void> { if (!this.config.enabled) { console.error('Monitoring is disabled in configuration'); return; } // Initialize user information and notification channel await this.initialize(); this.scheduler.start(); } /** * Stops the monitoring process */ stop(): void { this.scheduler.stop(); } /** * Updates the monitoring configuration * @param config New monitoring configuration */ updateConfig(config: MonitoringConfig): void { this.config = config; this.scheduler.updateConfig(config); } /** * Checks if the monitor is running * @returns True if the monitor is running, false otherwise */ isRunning(): boolean { return this.scheduler.isRunning(); } /** * Runs the monitoring process immediately */ async runNow(): Promise<void> { console.error("Running monitoring process immediately..."); await this.monitorChannels(); console.error("Immediate monitoring process completed."); } /** * Main monitoring function that checks channels for topics of interest */ private async monitorChannels(): Promise<void> { try { // Get channel IDs for the channels we want to monitor await this.refreshChannelCache(); // Process each channel for (const channelName of this.config.channels) { await this.processChannel(channelName); } } catch (error) { console.error('Error in monitorChannels:', error); } } /** * Refreshes the cache of channel names to IDs */ private async refreshChannelCache(): Promise<void> { try { // Get all channels const response = await this.client.getChannels(100, 0); // Update the cache for (const channel of response.channels) { this.channelCache.set(channel.name, channel.id); } } catch (error) { console.error('Error refreshing channel cache:', error); throw error; } } /** * Processes a single channel for topics of interest * @param channelName Name of the channel to process */ private async processChannel(channelName: string): Promise<void> { try { // Get the channel ID const channelId = this.channelCache.get(channelName); if (!channelId) { console.error(`Channel not found: ${channelName}`); return; } // Get recent posts const postsResponse = await this.client.getPostsForChannel( channelId, this.config.messageLimit, 0 ); // Convert posts object to array const posts: Post[] = Object.values(postsResponse.posts || {}); if (posts.length === 0) { console.error(`No posts found in channel: ${channelName}`); return; } // Find posts that match the topics const relevantPosts = findRelevantPosts(posts, this.config.topics); if (relevantPosts.length === 0) { console.error(`No relevant posts found in channel: ${channelName}`); return; } console.error(`Found ${relevantPosts.length} relevant posts in channel: ${channelName}`); // Create and send notification const userId = this.config.userId || this.currentUserId; const username = this.currentUsername || 'user'; if (!userId) { console.error('No user ID available for notification'); return; } const notificationMessage = createNotificationMessage( relevantPosts, channelName, username // Use username instead of userId ); if (notificationMessage) { const channelId = this.config.notificationChannelId || this.directMessageChannelId; if (!channelId) { console.error('No notification channel ID available'); return; } await this.client.createPost( channelId, notificationMessage ); console.error(`Sent notification for channel: ${channelName}`); } } catch (error) { console.error(`Error processing channel ${channelName}:`, error); } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/pvev/mattermost-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server