Skip to main content
Glama
graph-advanced-features.ts14.2 kB
import { Client } from '@microsoft/microsoft-graph-client'; import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import { z } from 'zod'; // Advanced Graph API features: Batch operations, webhooks, delta queries 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() }; } } // Type definitions for advanced features export interface BatchRequest { id?: string; method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'; url: string; headers?: Record<string, string>; body?: any; } export interface BatchResponse { responses: Array<{ id: string; status: number; headers: Record<string, string>; body: any; }>; executedAt: string; totalRequests: number; successCount: number; errorCount: number; } export interface DeltaQueryResponse { value: any[]; deltaToken: string; deltaLink?: string; nextLink?: string; hasMoreChanges: boolean; changeCount: number; queriedAt: string; } export interface WebhookSubscription { resource: string; changeTypes: ('created' | 'updated' | 'deleted')[]; notificationUrl: string; expirationDateTime?: string; clientState?: string; lifecycleNotificationUrl?: string; tlsVersion?: 'v1_0' | 'v1_1' | 'v1_2' | 'v1_3'; } export interface SubscriptionResponse { id: string; resource: string; changeType: string; notificationUrl: string; expirationDateTime: string; clientState?: string; createdAt?: string; updatedAt?: string; } export interface SearchQuery { entityTypes: ('message' | 'event' | 'drive' | 'driveItem' | 'list' | 'listItem' | 'site' | 'person')[]; queryString: string; from?: number; size?: number; fields?: string[]; sortProperties?: Array<{ name: string; isDescending?: boolean; }>; aggregations?: Array<{ field: string; size?: number; bucketDefinition?: any; }>; queryAlterationOptions?: { enableSuggestion?: boolean; enableModification?: boolean; }; } export interface SearchResponse { hits: Array<{ hitId: string; rank: number; summary: string; resource: any; }>; totalCount: number; moreResultsAvailable: boolean; aggregations: any[]; queryAlterationResponse?: any; searchedAt: string; } export interface ProcessedNotification { subscriptionId: string; changeType: string; resource: string; resourceData?: any; subscriptionExpirationDateTime?: string; clientState?: string; tenantId?: string; processedAt: string; } // Zod schemas for validation export const batchRequestSchema = z.object({ requests: z.array(z.object({ id: z.string().optional(), method: z.enum(['GET', 'POST', 'PATCH', 'PUT', 'DELETE']), url: z.string(), headers: z.record(z.string(), z.string()).optional(), body: z.any().optional() })).min(1).max(20) }); export const deltaQuerySchema = z.object({ resource: z.string().describe('Graph resource path (e.g., /users, /groups)'), deltaToken: z.string().optional().describe('Delta token from previous query') }); export const webhookSubscriptionSchema = z.object({ resource: z.string().describe('Graph resource to monitor'), changeTypes: z.array(z.enum(['created', 'updated', 'deleted'])).describe('Types of changes to monitor'), notificationUrl: z.string().url().describe('Webhook endpoint URL'), expirationDateTime: z.string().optional().describe('Subscription expiration (ISO 8601)'), clientState: z.string().optional().describe('Client state for validation'), lifecycleNotificationUrl: z.string().url().optional().describe('Lifecycle notification URL'), tlsVersion: z.enum(['v1_0', 'v1_1', 'v1_2', 'v1_3']).optional().describe('Minimum TLS version') }); 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') });

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/DynamicEndpoints/m365-core-mcp'

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