execute_graph_search
Search Microsoft 365 content including emails, files, messages, and calendar events using advanced queries to find specific information across the platform.
Instructions
Execute advanced search queries across Microsoft 365 content including emails, files, messages, and calendar events.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| entityTypes | Yes | ||
| queryString | Yes | Search query string | |
| from | No | Starting index for results | |
| size | No | Number of results to return | |
| fields | No | Fields to include in results | |
| sortProperties | No | Sort properties | |
| aggregations | No | Aggregation definitions | |
| queryAlterationOptions | No | Query alteration options |
Implementation Reference
- Main handler function implementing Microsoft Graph Search API via /search/query endpoint. Processes search query parameters and returns formatted search results including hits, aggregations, and metadata.async executeSearch(query: SearchQuery): Promise<SearchResponse> { const searchPayload = { requests: [{ entityTypes: query.entityTypes, query: { queryString: query.queryString }, from: query.from || 0, size: query.size || 25, fields: query.fields, sortProperties: query.sortProperties, aggregations: query.aggregations, queryAlterationOptions: query.queryAlterationOptions }] }; try { const response = await this.graphClient .api('/search/query') .post(searchPayload); const searchResults = response.value[0]; return { hits: searchResults.hitsContainers[0]?.hits || [], totalCount: searchResults.hitsContainers[0]?.total || 0, moreResultsAvailable: searchResults.hitsContainers[0]?.moreResultsAvailable || false, aggregations: searchResults.hitsContainers[0]?.aggregations || [], queryAlterationResponse: searchResults.queryAlterationResponse, searchedAt: new Date().toISOString() }; } catch (error) { throw new McpError( ErrorCode.InternalError, `Search query failed: ${error instanceof Error ? error.message : 'Unknown error'}` ); } }
- src/server.ts:1493-1514 (registration)MCP tool registration for 'execute_graph_search'. Instantiates GraphAdvancedFeatures class and delegates to its executeSearch method.this.server.tool( "execute_graph_search", "Execute advanced search queries across Microsoft 365 content including emails, files, messages, and calendar events.", searchQuerySchema.shape, {"readOnlyHint":true,"destructiveHint":false,"idempotentHint":true}, wrapToolHandler(async (args: any) => { this.validateCredentials(); try { const advancedFeatures = new GraphAdvancedFeatures(this.getGraphClient(), this.getAccessToken.bind(this)); const result = await advancedFeatures.executeSearch(args); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Error executing search: ${error instanceof Error ? error.message : 'Unknown error'}` ); } }) );
- Zod input schema defining parameters for Microsoft Graph Search API including entity types, query string, pagination, sorting, aggregations, and query alterations.export const searchQuerySchema = z.object({ entityTypes: z.array(z.enum(['message', 'event', 'drive', 'driveItem', 'list', 'listItem', 'site', 'person'])), queryString: z.string().describe('Search query string'), from: z.number().min(0).optional().describe('Starting index for results'), size: z.number().min(1).max(1000).optional().describe('Number of results to return'), fields: z.array(z.string()).optional().describe('Fields to include in results'), sortProperties: z.array(z.object({ name: z.string(), isDescending: z.boolean().optional() })).optional().describe('Sort properties'), aggregations: z.array(z.object({ field: z.string(), size: z.number().optional(), bucketDefinition: z.any().optional() })).optional().describe('Aggregation definitions'), queryAlterationOptions: z.object({ enableSuggestion: z.boolean().optional(), enableModification: z.boolean().optional() }).optional().describe('Query alteration options') });
- src/tool-metadata.ts:223-227 (schema)Tool metadata providing description, title, and annotations (read-only, non-destructive, idempotent) used for MCP tool discovery and UI hints.execute_graph_search: { description: "Execute advanced search queries across Microsoft 365 content including emails, files, messages, and calendar events.", title: "Graph Search", annotations: { title: "Graph Search", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } },
- GraphAdvancedFeatures class providing advanced Graph API capabilities including batching, delta queries, subscriptions, and search. Used by the tool handler.export class GraphAdvancedFeatures { private graphClient: Client; private getAccessToken: (scope: string) => Promise<string>; constructor(graphClient: Client, getAccessToken: (scope: string) => Promise<string>) { this.graphClient = graphClient; this.getAccessToken = getAccessToken; } // Batch Operations - Execute multiple Graph requests in a single call async executeBatch(requests: BatchRequest[]): Promise<BatchResponse> { if (requests.length === 0) { throw new McpError(ErrorCode.InvalidParams, 'At least one request is required for batch operation'); } if (requests.length > 20) { throw new McpError(ErrorCode.InvalidParams, 'Maximum 20 requests allowed per batch'); } const batchPayload = { requests: requests.map((req, index) => ({ id: req.id || index.toString(), method: req.method.toUpperCase(), url: req.url, headers: req.headers || {}, body: req.body })) }; try { const response = await this.graphClient .api('/$batch') .post(batchPayload); return { responses: response.responses, executedAt: new Date().toISOString(), totalRequests: requests.length, successCount: response.responses.filter((r: any) => r.status >= 200 && r.status < 300).length, errorCount: response.responses.filter((r: any) => r.status >= 400).length }; } catch (error) { throw new McpError( ErrorCode.InternalError, `Batch operation failed: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } // Delta Queries - Efficiently track changes to Graph resources async executeDeltaQuery(resource: string, deltaToken?: string): Promise<DeltaQueryResponse> { let apiPath = resource; // Add delta function to the path if (!apiPath.includes('/delta')) { apiPath = apiPath.endsWith('/') ? `${apiPath}delta` : `${apiPath}/delta`; } try { let request = this.graphClient.api(apiPath); // If we have a delta token, use it to get only changes since last query if (deltaToken) { request = request.query({ $deltatoken: deltaToken }); } const response = await request.get(); // Extract delta link and delta token from response const deltaLink = response['@odata.deltaLink']; const nextLink = response['@odata.nextLink']; let extractedDeltaToken = ''; if (deltaLink) { const tokenMatch = deltaLink.match(/\$deltatoken=([^&]+)/); extractedDeltaToken = tokenMatch ? decodeURIComponent(tokenMatch[1]) : ''; } return { value: response.value || [], deltaToken: extractedDeltaToken, deltaLink: deltaLink, nextLink: nextLink, hasMoreChanges: !!nextLink, changeCount: response.value ? response.value.length : 0, queriedAt: new Date().toISOString() }; } catch (error) { throw new McpError( ErrorCode.InternalError, `Delta query failed: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } // Webhook Subscriptions - Set up real-time change notifications async createSubscription(subscription: WebhookSubscription): Promise<SubscriptionResponse> { const subscriptionPayload: any = { changeType: subscription.changeTypes.join(','), notificationUrl: subscription.notificationUrl, resource: subscription.resource, expirationDateTime: subscription.expirationDateTime || this.getDefaultExpiration(), clientState: subscription.clientState, latestSupportedTlsVersion: subscription.tlsVersion || 'v1_2' }; // Add lifecycle notification URL if provided if (subscription.lifecycleNotificationUrl) { subscriptionPayload.lifecycleNotificationUrl = subscription.lifecycleNotificationUrl; } try { const response = await this.graphClient .api('/subscriptions') .post(subscriptionPayload); return { id: response.id, resource: response.resource, changeType: response.changeType, notificationUrl: response.notificationUrl, expirationDateTime: response.expirationDateTime, clientState: response.clientState, createdAt: new Date().toISOString() }; } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to create subscription: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } // Update existing subscription async updateSubscription(subscriptionId: string, updates: Partial<WebhookSubscription>): Promise<SubscriptionResponse> { const updatePayload: any = {}; if (updates.expirationDateTime) { updatePayload.expirationDateTime = updates.expirationDateTime; } if (updates.notificationUrl) { updatePayload.notificationUrl = updates.notificationUrl; } try { const response = await this.graphClient .api(`/subscriptions/${subscriptionId}`) .patch(updatePayload); return { id: response.id, resource: response.resource, changeType: response.changeType, notificationUrl: response.notificationUrl, expirationDateTime: response.expirationDateTime, clientState: response.clientState, updatedAt: new Date().toISOString() }; } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to update subscription: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } // Delete subscription async deleteSubscription(subscriptionId: string): Promise<{ deleted: boolean; deletedAt: string }> { try { await this.graphClient .api(`/subscriptions/${subscriptionId}`) .delete(); return { deleted: true, deletedAt: new Date().toISOString() }; } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to delete subscription: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } // List all subscriptions async listSubscriptions(): Promise<SubscriptionResponse[]> { try { const response = await this.graphClient .api('/subscriptions') .get(); return response.value.map((sub: any) => ({ id: sub.id, resource: sub.resource, changeType: sub.changeType, notificationUrl: sub.notificationUrl, expirationDateTime: sub.expirationDateTime, clientState: sub.clientState })); } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to list subscriptions: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } // Advanced Search - Use Microsoft Search API async executeSearch(query: SearchQuery): Promise<SearchResponse> { const searchPayload = { requests: [{ entityTypes: query.entityTypes, query: { queryString: query.queryString }, from: query.from || 0, size: query.size || 25, fields: query.fields, sortProperties: query.sortProperties, aggregations: query.aggregations, queryAlterationOptions: query.queryAlterationOptions }] }; try { const response = await this.graphClient .api('/search/query') .post(searchPayload); const searchResults = response.value[0]; return { hits: searchResults.hitsContainers[0]?.hits || [], totalCount: searchResults.hitsContainers[0]?.total || 0, moreResultsAvailable: searchResults.hitsContainers[0]?.moreResultsAvailable || false, aggregations: searchResults.hitsContainers[0]?.aggregations || [], queryAlterationResponse: searchResults.queryAlterationResponse, searchedAt: new Date().toISOString() }; } catch (error) { throw new McpError( ErrorCode.InternalError, `Search query failed: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } // Get default expiration time for subscriptions (maximum allowed) private getDefaultExpiration(): string { const now = new Date(); // Most subscriptions have a maximum lifetime of 4230 minutes (about 3 days) now.setMinutes(now.getMinutes() + 4230); return now.toISOString(); } // Validate webhook notification (for webhook endpoint implementation) validateWebhookNotification(notification: any, clientState?: string): boolean { if (!notification || !notification.value) { return false; } // Validate client state if provided if (clientState && notification.clientState !== clientState) { return false; } // Validate required fields const requiredFields = ['subscriptionId', 'changeType', 'resource']; for (const field of requiredFields) { if (!notification[field]) { return false; } } return true; } // Process webhook notification processWebhookNotification(notification: any): ProcessedNotification { return { subscriptionId: notification.subscriptionId, changeType: notification.changeType, resource: notification.resource, resourceData: notification.resourceData, subscriptionExpirationDateTime: notification.subscriptionExpirationDateTime, clientState: notification.clientState, tenantId: notification.tenantId, processedAt: new Date().toISOString() }; } }