Skip to main content
Glama
chats.ts13 kB
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import type { GraphService } from "../services/graph.js"; import type { Chat, ChatMessage, ChatSummary, ConversationMember, CreateChatPayload, GraphApiResponse, MessageSummary, User, } from "../types/graph.js"; import { markdownToHtml } from "../utils/markdown.js"; import { processMentionsInHtml } from "../utils/users.js"; export function registerChatTools(server: McpServer, graphService: GraphService) { // List user's chats server.tool( "list_chats", "List all recent chats (1:1 conversations and group chats) that the current user participates in. Returns chat topics, types, and participant information.", {}, async () => { try { // Build query parameters const queryParams: string[] = ["$expand=members"]; const queryString = queryParams.join("&"); const client = await graphService.getClient(); const response = (await client .api(`/me/chats?${queryString}`) .get()) as GraphApiResponse<Chat>; if (!response?.value?.length) { return { content: [ { type: "text", text: "No chats found.", }, ], }; } const chatList: ChatSummary[] = response.value.map((chat: Chat) => ({ id: chat.id, topic: chat.topic || "No topic", chatType: chat.chatType, members: chat.members?.map((member: ConversationMember) => member.displayName).join(", ") || "No members", })); return { content: [ { type: "text", text: JSON.stringify(chatList, null, 2), }, ], }; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; return { content: [ { type: "text", text: `❌ Error: ${errorMessage}`, }, ], }; } } ); // Get chat messages server.tool( "get_chat_messages", "Retrieve recent messages from a specific chat conversation. Returns message content, sender information, and timestamps.", { chatId: z.string().describe("Chat ID (e.g. 19:meeting_Njhi..j@thread.v2"), limit: z .number() .min(1) .max(50) .optional() .default(20) .describe("Number of messages to retrieve"), since: z.string().optional().describe("Get messages since this ISO datetime"), until: z.string().optional().describe("Get messages until this ISO datetime"), fromUser: z.string().optional().describe("Filter messages from specific user ID"), orderBy: z .enum(["createdDateTime", "lastModifiedDateTime"]) .optional() .default("createdDateTime") .describe("Sort order"), descending: z .boolean() .optional() .default(true) .describe("Sort in descending order (newest first)"), }, async ({ chatId, limit, since, until, fromUser, orderBy, descending }) => { try { const client = await graphService.getClient(); // Build query parameters const queryParams: string[] = [`$top=${limit}`]; // Add ordering - Graph API only supports descending order for datetime fields in chat messages if ((orderBy === "createdDateTime" || orderBy === "lastModifiedDateTime") && !descending) { return { content: [ { type: "text", text: `❌ Error: QueryOptions to order by '${orderBy === "createdDateTime" ? "CreatedDateTime" : "LastModifiedDateTime"}' in 'Ascending' direction is not supported.`, }, ], }; } const sortDirection = descending ? "desc" : "asc"; queryParams.push(`$orderby=${orderBy} ${sortDirection}`); // Add filters (only user filter is supported reliably) const filters: string[] = []; if (fromUser) { filters.push(`from/user/id eq '${fromUser}'`); } if (filters.length > 0) { queryParams.push(`$filter=${filters.join(" and ")}`); } const queryString = queryParams.join("&"); const response = (await client .api(`/me/chats/${chatId}/messages?${queryString}`) .get()) as GraphApiResponse<ChatMessage>; if (!response?.value?.length) { return { content: [ { type: "text", text: "No messages found in this chat with the specified filters.", }, ], }; } // Apply client-side date filtering since server-side filtering is not supported let filteredMessages = response.value; if (since || until) { filteredMessages = response.value.filter((message: ChatMessage) => { if (!message.createdDateTime) return true; const messageDate = new Date(message.createdDateTime); if (since) { const sinceDate = new Date(since); if (messageDate <= sinceDate) return false; } if (until) { const untilDate = new Date(until); if (messageDate >= untilDate) return false; } return true; }); } const messageList: MessageSummary[] = filteredMessages.map((message: ChatMessage) => ({ id: message.id, content: message.body?.content, from: message.from?.user?.displayName, createdDateTime: message.createdDateTime, })); return { content: [ { type: "text", text: JSON.stringify( { filters: { since, until, fromUser }, filteringMethod: since || until ? "client-side" : "server-side", totalReturned: messageList.length, hasMore: !!response["@odata.nextLink"], messages: messageList, }, null, 2 ), }, ], }; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; return { content: [ { type: "text", text: `❌ Error: ${errorMessage}`, }, ], }; } } ); // Send chat message server.tool( "send_chat_message", "Send a message to a specific chat conversation. Supports text and markdown formatting, mentions, and importance levels.", { chatId: z.string().describe("Chat ID"), message: z.string().describe("Message content"), importance: z.enum(["normal", "high", "urgent"]).optional().describe("Message importance"), format: z.enum(["text", "markdown"]).optional().describe("Message format (text or markdown)"), mentions: z .array( z.object({ mention: z .string() .describe("The @mention text (e.g., 'john.doe' or 'john.doe@company.com')"), userId: z.string().describe("Azure AD User ID of the mentioned user"), }) ) .optional() .describe("Array of @mentions to include in the message"), }, async ({ chatId, message, importance = "normal", format = "text", mentions }) => { try { const client = await graphService.getClient(); // Process message content based on format let content: string; let contentType: "text" | "html"; if (format === "markdown") { content = await markdownToHtml(message); contentType = "html"; } else { content = message; contentType = "text"; } // Process @mentions if provided const mentionMappings: Array<{ mention: string; userId: string; displayName: string }> = []; if (mentions && mentions.length > 0) { // Convert provided mentions to mappings with display names for (const mention of mentions) { try { // Get user info to get display name const userResponse = await client .api(`/users/${mention.userId}`) .select("displayName") .get(); mentionMappings.push({ mention: mention.mention, userId: mention.userId, displayName: userResponse.displayName || mention.mention, }); } catch (_error) { console.warn( `Could not resolve user ${mention.userId}, using mention text as display name` ); mentionMappings.push({ mention: mention.mention, userId: mention.userId, displayName: mention.mention, }); } } } // Process mentions in HTML content let finalMentions: Array<{ id: number; mentionText: string; mentioned: { user: { id: string } }; }> = []; if (mentionMappings.length > 0) { const result = processMentionsInHtml(content, mentionMappings); content = result.content; finalMentions = result.mentions; // Ensure we're using HTML content type when mentions are present contentType = "html"; } // Build message payload const messagePayload: any = { body: { content, contentType, }, importance, }; if (finalMentions.length > 0) { messagePayload.mentions = finalMentions; } const result = (await client .api(`/me/chats/${chatId}/messages`) .post(messagePayload)) as ChatMessage; // Build success message const successText = `✅ Message sent successfully. Message ID: ${result.id}${ finalMentions.length > 0 ? `\n📱 Mentions: ${finalMentions.map((m) => m.mentionText).join(", ")}` : "" }`; return { content: [ { type: "text" as const, text: successText, }, ], }; } catch (error: any) { return { content: [ { type: "text" as const, text: `❌ Failed to send message: ${error.message}`, }, ], isError: true, }; } } ); // Create new chat (1:1 or group) server.tool( "create_chat", "Create a new chat conversation. Can be a 1:1 chat (with one other user) or a group chat (with multiple users). Group chats can optionally have a topic.", { userEmails: z.array(z.string()).describe("Array of user email addresses to add to chat"), topic: z.string().optional().describe("Chat topic (for group chats)"), }, async ({ userEmails, topic }) => { try { const client = await graphService.getClient(); // Get current user ID const me = (await client.api("/me").get()) as User; // Create members array const members: ConversationMember[] = [ { "@odata.type": "#microsoft.graph.aadUserConversationMember", user: { id: me?.id, }, roles: ["owner"], } as ConversationMember, ]; // Add other users as members for (const email of userEmails) { const user = (await client.api(`/users/${email}`).get()) as User; members.push({ "@odata.type": "#microsoft.graph.aadUserConversationMember", user: { id: user?.id, }, roles: ["member"], } as ConversationMember); } const chatData: CreateChatPayload = { chatType: userEmails.length === 1 ? "oneOnOne" : "group", members, }; if (topic && userEmails.length > 1) { chatData.topic = topic; } const newChat = (await client.api("/chats").post(chatData)) as Chat; return { content: [ { type: "text", text: `✅ Chat created successfully. Chat ID: ${newChat?.id}`, }, ], }; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; return { content: [ { type: "text", text: `❌ Error: ${errorMessage}`, }, ], }; } } ); }

Implementation Reference

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/floriscornel/teams-mcp'

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