Skip to main content
Glama
by jhanglim
index.ts19.2 kB
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import fetch from "node-fetch"; // Mattermost 설정 const MATTERMOST_URL = process.env.MATTERMOST_URL || "https://your-mattermost-server.com"; const MATTERMOST_TOKEN = process.env.MATTERMOST_TOKEN || ""; interface MattermostConfig { url: string; token: string; } interface MattermostPost { id: string; message: string; user_id: string; channel_id: string; create_at: number; update_at: number; } interface MattermostSearchResult { order: string[]; posts: Record<string, MattermostPost>; } interface MattermostPostsResult { order: string[]; posts: Record<string, MattermostPost>; } interface MattermostUser { id: string; username: string; first_name: string; last_name: string; nickname: string; email?: string; } // KST 변환 헬퍼 함수 function formatTimestamp(timestamp: number) { const kstDate = new Date(timestamp + 9 * 60 * 60 * 1000); return kstDate.toISOString().replace('Z', '+09:00'); } class MattermostClient { private config: MattermostConfig; private userCache: Map<string, any> = new Map(); constructor(config: MattermostConfig) { this.config = config; } private async request(endpoint: string, options: any = {}) { const url = `${this.config.url}/api/v4${endpoint}`; const response = await fetch(url, { ...options, headers: { "Authorization": `Bearer ${this.config.token}`, "Content-Type": "application/json", ...options.headers, }, }); if (!response.ok) { throw new Error(`Mattermost API error: ${response.status} ${response.statusText}`); } return await response.json(); } async searchPosts(terms: string, isOrSearch: boolean = false): Promise<MattermostSearchResult> { return await this.request("/posts/search", { method: "POST", body: JSON.stringify({ terms, is_or_search: isOrSearch, }), }) as MattermostSearchResult; } async searchUsers(term: string): Promise<MattermostUser[]> { return await this.request("/users/search", { method: "POST", body: JSON.stringify({ term, allow_inactive: false, }), }) as MattermostUser[]; } async getUserByUsername(username: string): Promise<MattermostUser> { return await this.request(`/users/username/${username}`) as MattermostUser; } async getUser(userId: string) { // 캐시 확인 if (this.userCache.has(userId)) { return this.userCache.get(userId); } try { const user = await this.request(`/users/${userId}`); this.userCache.set(userId, user); return user; } catch (e) { return null; } } async getUsersInfo(userIds: string[]) { const userMap = new Map(); for (const userId of userIds) { const user = await this.getUser(userId); if (user) { userMap.set(userId, { username: user.username, name: `${user.first_name} ${user.last_name}`.trim() || user.nickname || user.username, }); } else { userMap.set(userId, { username: "unknown", name: "Unknown User" }); } } return userMap; } async getMe(): Promise<MattermostUser> { return await this.request("/users/me") as MattermostUser; } async getChannel(channelId: string) { return await this.request(`/channels/${channelId}`); } async getTeams() { return await this.request("/users/me/teams"); } async getChannelsForTeam(teamId: string) { return await this.request(`/users/me/teams/${teamId}/channels`); } async getPost(postId: string) { return await this.request(`/posts/${postId}`); } async getPostThread(postId: string): Promise<MattermostPostsResult> { return await this.request(`/posts/${postId}/thread`) as MattermostPostsResult; } async getChannelMessages(channelId: string, page: number = 0, perPage: number = 60): Promise<MattermostPostsResult> { return await this.request(`/channels/${channelId}/posts?page=${page}&per_page=${perPage}`) as MattermostPostsResult; } } const server = new Server( { name: "mattermost-mcp-server", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); const client = new MattermostClient({ url: MATTERMOST_URL, token: MATTERMOST_TOKEN, }); server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "get_current_user", description: "현재 토큰 소유자(나)의 정보를 조회합니다.", inputSchema: { type: "object", properties: {}, }, }, { name: "get_user_info", description: "사용자 ID로 사용자의 상세 정보를 조회합니다. username, 이름, 닉네임 등을 확인할 수 있습니다.", inputSchema: { type: "object", properties: { user_id: { type: "string", description: "조회할 사용자의 ID", }, }, required: ["user_id"], }, }, { name: "search_messages", description: "Mattermost에서 메시지를 검색합니다. 키워드, 사용자명(@username 또는 from:username), 날짜 등으로 검색할 수 있습니다. 검색 결과에는 자동으로 작성자의 이름(user_name)과 username이 포함됩니다.", inputSchema: { type: "object", properties: { query: { type: "string", description: "검색할 키워드 또는 검색어. 사용자명으로 검색하려면 'from:username' 또는 '@username' 형식 사용", }, is_or_search: { type: "boolean", description: "true인 경우 OR 검색, false인 경우 AND 검색 (기본값: false)", default: false, }, }, required: ["query"], }, }, { name: "search_user_messages", description: "특정 사용자의 메시지를 이름이나 username으로 검색합니다. '박찬우', 'cwpark' 등으로 검색 가능.", inputSchema: { type: "object", properties: { user_name: { type: "string", description: "검색할 사용자의 이름 또는 username (예: '박찬우', 'cwpark')", }, keyword: { type: "string", description: "추가로 검색할 키워드 (선택사항)", }, }, required: ["user_name"], }, }, { name: "search_users", description: "사용자를 이름, username, 닉네임으로 검색합니다.", inputSchema: { type: "object", properties: { search_term: { type: "string", description: "검색할 이름, username 또는 닉네임", }, }, required: ["search_term"], }, }, { name: "get_teams", description: "현재 사용자가 속한 모든 팀 목록을 가져옵니다.", inputSchema: { type: "object", properties: {}, }, }, { name: "get_channels", description: "특정 팀의 채널 목록을 가져옵니다.", inputSchema: { type: "object", properties: { team_id: { type: "string", description: "팀 ID", }, }, required: ["team_id"], }, }, { name: "get_channel_messages", description: "특정 채널의 최근 메시지들을 가져옵니다. 결과에는 자동으로 작성자의 이름(user_name)과 username이 포함됩니다.", inputSchema: { type: "object", properties: { channel_id: { type: "string", description: "채널 ID", }, page: { type: "number", description: "페이지 번호 (기본값: 0)", default: 0, }, per_page: { type: "number", description: "페이지당 메시지 수 (기본값: 60)", default: 60, }, }, required: ["channel_id"], }, }, { name: "get_post_thread", description: "특정 게시물의 전체 스레드를 가져옵니다. 결과에는 자동으로 작성자의 이름(user_name)과 username이 포함됩니다.", inputSchema: { type: "object", properties: { post_id: { type: "string", description: "게시물 ID", }, }, required: ["post_id"], }, }, ], }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; if (!args) { throw new Error("Arguments are required"); } switch (name) { case "get_current_user": { const me = await client.getMe(); return { content: [ { type: "text", text: JSON.stringify({ id: me.id, username: me.username, email: me.email || "", first_name: me.first_name || "", last_name: me.last_name || "", nickname: me.nickname || "", full_name: `${me.first_name} ${me.last_name}`.trim() || me.nickname || me.username, }, null, 2), }, ], }; } case "get_user_info": { const userId = args.user_id as string; const user = await client.getUser(userId); if (!user) { return { content: [ { type: "text", text: JSON.stringify({ error: "User not found", user_id: userId, }, null, 2), }, ], }; } return { content: [ { type: "text", text: JSON.stringify({ id: user.id, username: user.username, email: user.email || "", first_name: user.first_name || "", last_name: user.last_name || "", nickname: user.nickname || "", full_name: `${user.first_name} ${user.last_name}`.trim() || user.nickname || user.username, }, null, 2), }, ], }; } case "search_messages": { const query = args.query as string; const isOrSearch = (args.is_or_search as boolean) || false; const result = await client.searchPosts(query, isOrSearch); // 고유한 user_id 추출 const uniqueUserIds = [...new Set(result.order?.map((postId: string) => result.posts[postId].user_id) || [])]; // 사용자 정보 일괄 조회 const userMap = await client.getUsersInfo(uniqueUserIds); // 검색 결과 포맷팅 const posts = result.order?.map((postId: string) => { const post = result.posts[postId]; const createTime = formatTimestamp(post.create_at); const updateTime = formatTimestamp(post.update_at); const userInfo = userMap.get(post.user_id); return { id: post.id, message: post.message, user_id: post.user_id, username: userInfo?.username || "unknown", user_name: userInfo?.name || "Unknown User", channel_id: post.channel_id, create_at: createTime, update_at: updateTime, }; }) || []; return { content: [ { type: "text", text: JSON.stringify({ total_count: posts.length, posts: posts, }, null, 2), }, ], }; } case "search_user_messages": { const userName = args.user_name as string; const keyword = (args.keyword as string) || ""; // 먼저 사용자 검색 let users: MattermostUser[] = []; try { // username으로 직접 조회 시도 const user = await client.getUserByUsername(userName); users = [user]; } catch { // 실패하면 검색으로 시도 users = await client.searchUsers(userName); } if (users.length === 0) { return { content: [ { type: "text", text: JSON.stringify({ error: `사용자 '${userName}'를 찾을 수 없습니다.`, total_count: 0, posts: [], }, null, 2), }, ], }; } // 첫 번째 매칭된 사용자의 메시지 검색 const user = users[0]; const searchQuery = keyword ? `from:${user.username} ${keyword}` : `from:${user.username}`; const result = await client.searchPosts(searchQuery, false); // 고유한 user_id 추출 및 사용자 정보 조회 const uniqueUserIds = [...new Set(result.order?.map((postId: string) => result.posts[postId].user_id) || [])]; const userMap = await client.getUsersInfo(uniqueUserIds); const posts = result.order?.map((postId: string) => { const post = result.posts[postId]; const createTime = formatTimestamp(post.create_at); const updateTime = formatTimestamp(post.update_at); const userInfo = userMap.get(post.user_id); return { id: post.id, message: post.message, user_id: post.user_id, username: userInfo?.username || "unknown", user_name: userInfo?.name || "Unknown User", channel_id: post.channel_id, create_at: createTime, update_at: updateTime, }; }) || []; return { content: [ { type: "text", text: JSON.stringify({ found_user: { id: user.id, username: user.username, name: `${user.first_name} ${user.last_name}`.trim() || user.nickname, }, total_count: posts.length, posts: posts, }, null, 2), }, ], }; } case "search_users": { const searchTerm = args.search_term as string; const users = await client.searchUsers(searchTerm); return { content: [ { type: "text", text: JSON.stringify({ total_count: users.length, users: users.map(u => ({ id: u.id, username: u.username, name: `${u.first_name} ${u.last_name}`.trim() || u.nickname, nickname: u.nickname, })), }, null, 2), }, ], }; } case "get_teams": { const teams = await client.getTeams(); return { content: [ { type: "text", text: JSON.stringify(teams, null, 2), }, ], }; } case "get_channels": { const teamId = args.team_id as string; const channels = await client.getChannelsForTeam(teamId); return { content: [ { type: "text", text: JSON.stringify(channels, null, 2), }, ], }; } case "get_channel_messages": { const channelId = args.channel_id as string; const page = (args.page as number) || 0; const perPage = (args.per_page as number) || 60; const messages = await client.getChannelMessages(channelId, page, perPage); // 고유한 user_id 추출 및 사용자 정보 조회 const uniqueUserIds = [...new Set(messages.order?.map((postId: string) => messages.posts[postId].user_id) || [])]; const userMap = await client.getUsersInfo(uniqueUserIds); const posts = messages.order?.map((postId: string) => { const post = messages.posts[postId]; const createTime = formatTimestamp(post.create_at); const userInfo = userMap.get(post.user_id); return { id: post.id, message: post.message, user_id: post.user_id, username: userInfo?.username || "unknown", user_name: userInfo?.name || "Unknown User", create_at: createTime, }; }) || []; return { content: [ { type: "text", text: JSON.stringify({ channel_id: channelId, posts: posts, }, null, 2), }, ], }; } case "get_post_thread": { const postId = args.post_id as string; const thread = await client.getPostThread(postId); // 고유한 user_id 추출 및 사용자 정보 조회 const uniqueUserIds = [...new Set(thread.order?.map((postId: string) => thread.posts[postId].user_id) || [])]; const userMap = await client.getUsersInfo(uniqueUserIds); const posts = thread.order?.map((postId: string) => { const post = thread.posts[postId]; const createTime = formatTimestamp(post.create_at); const userInfo = userMap.get(post.user_id); return { id: post.id, message: post.message, user_id: post.user_id, username: userInfo?.username || "unknown", user_name: userInfo?.name || "Unknown User", create_at: createTime, }; }) || []; return { content: [ { type: "text", text: JSON.stringify({ posts: posts, }, null, 2), }, ], }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Mattermost MCP Server running on stdio"); } main().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); });

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/jhanglim/mattermost-mcp-server'

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