Skip to main content
Glama
search.ts19.5 kB
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import type { GraphService } from "../services/graph.js"; import type { SearchHit, SearchRequest, SearchResponse } from "../types/graph.js"; export function registerSearchTools(server: McpServer, graphService: GraphService) { // Search messages across Teams using Microsoft Search API server.tool( "search_messages", "Search for messages across all Microsoft Teams channels and chats using Microsoft Search API. Supports advanced KQL syntax for filtering by sender, mentions, attachments, and more.", { query: z .string() .describe( "Search query. Supports KQL syntax like 'from:user mentions:userId hasAttachment:true'" ), scope: z .enum(["all", "channels", "chats"]) .optional() .default("all") .describe("Scope of search"), limit: z .number() .min(1) .max(100) .optional() .default(25) .describe("Number of results to return"), enableTopResults: z .boolean() .optional() .default(true) .describe("Enable relevance-based ranking"), }, async ({ query, scope, limit, enableTopResults }) => { try { const client = await graphService.getClient(); // Build the search request const searchRequest: SearchRequest = { entityTypes: ["chatMessage"], query: { queryString: query, }, from: 0, size: limit, enableTopResults, }; // Add scope-specific filters to the query if needed let enhancedQuery = query; if (scope === "channels") { enhancedQuery = `${query} AND (channelIdentity/channelId:*)`; } else if (scope === "chats") { enhancedQuery = `${query} AND (chatId:* AND NOT channelIdentity/channelId:*)`; } searchRequest.query.queryString = enhancedQuery; const response = (await client .api("/search/query") .post({ requests: [searchRequest] })) as SearchResponse; if (!response?.value?.length || !response.value[0]?.hitsContainers?.length) { return { content: [ { type: "text", text: "No messages found matching your search criteria.", }, ], }; } const hits = response.value[0].hitsContainers[0].hits; const searchResults = hits.map((hit: SearchHit) => ({ id: hit.resource.id, summary: hit.summary, rank: hit.rank, content: hit.resource.body?.content || "No content", from: hit.resource.from?.user?.displayName || "Unknown", createdDateTime: hit.resource.createdDateTime, chatId: hit.resource.chatId, teamId: hit.resource.channelIdentity?.teamId, channelId: hit.resource.channelIdentity?.channelId, })); return { content: [ { type: "text", text: JSON.stringify( { query, scope, totalResults: response.value[0].hitsContainers[0].total, results: searchResults, moreResultsAvailable: response.value[0].hitsContainers[0].moreResultsAvailable, }, null, 2 ), }, ], }; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; return { content: [ { type: "text", text: `❌ Error searching messages: ${errorMessage}`, }, ], }; } } ); // Get recent messages with advanced filtering server.tool( "get_recent_messages", "Get recent messages from across Teams with advanced filtering options. Can filter by time range, scope (channels vs chats), teams, channels, and users.", { hours: z .number() .min(1) .max(168) .optional() .default(24) .describe("Get messages from the last N hours (max 168 = 1 week)"), limit: z .number() .min(1) .max(100) .optional() .default(50) .describe("Maximum number of messages to return"), mentionsUser: z.string().optional().describe("Filter messages that mention this user ID"), fromUser: z.string().optional().describe("Filter messages from this user ID"), hasAttachments: z.boolean().optional().describe("Filter messages with attachments"), importance: z .enum(["low", "normal", "high", "urgent"]) .optional() .describe("Filter by message importance"), includeChannels: z.boolean().optional().default(true).describe("Include channel messages"), includeChats: z.boolean().optional().default(true).describe("Include chat messages"), teamIds: z.array(z.string()).optional().describe("Specific team IDs to search in"), keywords: z.string().optional().describe("Keywords to search for in message content"), }, async ({ hours, limit, mentionsUser, fromUser, hasAttachments, importance, includeChannels, includeChats, teamIds, keywords, }) => { try { const client = await graphService.getClient(); let attemptedAdvancedSearch = false; // Try using the Search API first for rich filtering if (keywords || mentionsUser || hasAttachments !== undefined || importance) { attemptedAdvancedSearch = true; // Calculate the date threshold const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString(); // Build KQL query for Microsoft Search API const queryParts: string[] = []; // Add time filter - use a more permissive date format queryParts.push(`sent>=${since.split("T")[0]}`); // Use just the date part // Add user filters if (mentionsUser) { queryParts.push(`mentions:${mentionsUser}`); } if (fromUser) { queryParts.push(`from:${fromUser}`); } // Add content filters if (hasAttachments !== undefined) { queryParts.push(`hasAttachment:${hasAttachments}`); } if (importance) { queryParts.push(`importance:${importance}`); } // Add keyword search if (keywords) { queryParts.push(`"${keywords}"`); } // If no specific filters, search for all recent messages if (queryParts.length === 1) { // Only has the time filter queryParts.push("*"); // Match all messages } const searchQuery = queryParts.join(" AND "); const searchRequest: SearchRequest = { entityTypes: ["chatMessage"], query: { queryString: searchQuery, }, from: 0, size: Math.min(limit, 100), enableTopResults: false, // For recent messages, prefer chronological order }; try { const response = (await client .api("/search/query") .post({ requests: [searchRequest] })) as SearchResponse; if (response?.value?.length && response.value[0]?.hitsContainers?.length) { const hits = response.value[0].hitsContainers[0].hits; const recentMessages = hits .filter((hit) => { // Apply scope filters const isChannelMessage = hit.resource.channelIdentity?.channelId; const isChatMessage = hit.resource.chatId && !isChannelMessage; if (!includeChannels && isChannelMessage) return false; if (!includeChats && isChatMessage) return false; // Apply team filter if specified if (teamIds?.length && isChannelMessage) { return teamIds.includes(hit.resource.channelIdentity?.teamId || ""); } return true; }) .map((hit: SearchHit) => ({ id: hit.resource.id, content: hit.resource.body?.content || "No content", from: hit.resource.from?.user?.displayName || "Unknown", fromUserId: hit.resource.from?.user?.id, createdDateTime: hit.resource.createdDateTime, chatId: hit.resource.chatId, teamId: hit.resource.channelIdentity?.teamId, channelId: hit.resource.channelIdentity?.channelId, type: hit.resource.channelIdentity?.channelId ? "channel" : "chat", })) .slice(0, limit); // Apply final limit after filtering // Check if Search API returned poor quality results (No content/Unknown) const poorQualityResults = recentMessages.filter( (msg) => msg.content === "No content" || msg.from === "Unknown" ).length; const qualityThreshold = 0.5; // If more than 50% of results are poor quality, fall back if ( recentMessages.length > 0 && poorQualityResults / recentMessages.length > qualityThreshold ) { console.log( "Search API returned poor quality results, falling back to direct queries" ); // Fall through to direct chat queries } else { return { content: [ { type: "text", text: JSON.stringify( { method: "search_api", timeRange: `Last ${hours} hours`, filters: { mentionsUser, fromUser, hasAttachments, importance, keywords, }, totalFound: recentMessages.length, messages: recentMessages, }, null, 2 ), }, ], }; } } } catch (searchError) { console.error("Search API failed, falling back to direct queries:", searchError); } } // Fallback: Get recent messages from user's chats directly // This method is more reliable but doesn't support advanced filtering const chatsResponse = await client.api("/me/chats?$expand=members").get(); const chats = chatsResponse?.value || []; const allMessages: Array<{ id: string; content: string; from: string; fromUserId?: string; createdDateTime: string; chatId: string; type: string; }> = []; const since = new Date(Date.now() - hours * 60 * 60 * 1000); // Get recent messages from each chat for (const chat of chats.slice(0, 10)) { // Limit to first 10 chats to avoid rate limits try { let queryString = `$top=${Math.min(limit, 50)}&$orderby=createdDateTime desc`; // Apply user filter if specified if (fromUser) { queryString += `&$filter=from/user/id eq '${fromUser}'`; } const messagesResponse = await client .api(`/me/chats/${chat.id}/messages?${queryString}`) .get(); const messages = messagesResponse?.value || []; for (const message of messages) { // Filter by time if (message.createdDateTime) { const messageDate = new Date(message.createdDateTime); if (messageDate < since) continue; } // Apply scope filter for chats if (!includeChats) { continue; // Skip chat messages if includeChats is false } // Apply keyword filter (simple text search) if ( keywords && message.body?.content && !message.body.content.toLowerCase().includes(keywords.toLowerCase()) ) { continue; } allMessages.push({ id: message.id || "", content: message.body?.content || "No content", from: message.from?.user?.displayName || "Unknown", fromUserId: message.from?.user?.id, createdDateTime: message.createdDateTime || "", chatId: message.chatId || "", type: "chat", }); if (allMessages.length >= limit) break; } if (allMessages.length >= limit) break; } catch (chatError) { console.error(`Error getting messages from chat ${chat.id}:`, chatError); } } // Sort by creation date (newest first) allMessages.sort( (a, b) => new Date(b.createdDateTime).getTime() - new Date(a.createdDateTime).getTime() ); return { content: [ { type: "text", text: JSON.stringify( { method: attemptedAdvancedSearch ? "direct_chat_queries_fallback" : "direct_chat_queries", timeRange: `Last ${hours} hours`, filters: { mentionsUser, fromUser, hasAttachments, importance, keywords, }, note: attemptedAdvancedSearch ? "Search API returned poor quality results, using direct chat queries as fallback" : "Using direct chat queries for better content reliability", totalFound: allMessages.slice(0, limit).length, messages: allMessages.slice(0, limit), }, null, 2 ), }, ], }; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; return { content: [ { type: "text", text: `❌ Error getting recent messages: ${errorMessage}`, }, ], }; } } ); // Search for messages mentioning the current user server.tool( "get_my_mentions", "Find all recent messages where the current user was mentioned (@mentioned) across Teams channels and chats.", { hours: z .number() .min(1) .max(168) .optional() .default(24) .describe("Get mentions from the last N hours"), limit: z .number() .min(1) .max(50) .optional() .default(20) .describe("Maximum number of mentions to return"), scope: z .enum(["all", "channels", "chats"]) .optional() .default("all") .describe("Scope of search"), }, async ({ hours, limit, scope }) => { try { const client = await graphService.getClient(); // Get current user ID first const me = await client.api("/me").get(); const userId = me?.id; if (!userId) { return { content: [ { type: "text", text: "❌ Error: Could not determine current user ID", }, ], }; } const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString(); // Build query to find mentions of current user const queryParts = [ `sent>=${since.split("T")[0]}`, // Use just the date part to avoid time parsing issues `mentions:${userId}`, ]; const searchQuery = queryParts.join(" AND "); const searchRequest: SearchRequest = { entityTypes: ["chatMessage"], query: { queryString: searchQuery, }, from: 0, size: Math.min(limit, 50), enableTopResults: false, }; const response = (await client .api("/search/query") .post({ requests: [searchRequest] })) as SearchResponse; if ( !response?.value?.length || !response.value[0]?.hitsContainers?.length || !response.value[0].hitsContainers[0]?.hits ) { return { content: [ { type: "text", text: "No recent mentions found.", }, ], }; } const hits = response.value[0].hitsContainers[0].hits || []; if (hits.length === 0) { return { content: [ { type: "text", text: "No recent mentions found.", }, ], }; } const mentions = hits .filter((hit) => { // Apply scope filters const isChannelMessage = hit.resource.channelIdentity?.channelId; const isChatMessage = hit.resource.chatId && !isChannelMessage; if (scope === "channels" && !isChannelMessage) return false; if (scope === "chats" && !isChatMessage) return false; return true; }) .map((hit: SearchHit) => ({ id: hit.resource.id, content: hit.resource.body?.content || "No content", summary: hit.summary, from: hit.resource.from?.user?.displayName || "Unknown", fromUserId: hit.resource.from?.user?.id, createdDateTime: hit.resource.createdDateTime, chatId: hit.resource.chatId, teamId: hit.resource.channelIdentity?.teamId, channelId: hit.resource.channelIdentity?.channelId, type: hit.resource.channelIdentity?.channelId ? "channel" : "chat", })); return { content: [ { type: "text", text: JSON.stringify( { timeRange: `Last ${hours} hours`, mentionedUser: me?.displayName || "Current User", scope, totalMentions: mentions.length, mentions, }, null, 2 ), }, ], }; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; return { content: [ { type: "text", text: `❌ Error getting mentions: ${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