create-calendar-event
Create calendar events in Microsoft 365 with details like attendees, times, and attachments to schedule meetings and appointments.
Instructions
Create one or more multi-value extended properties in a new or existing instance of a resource. The following user resources are supported: The following group resources are supported: See Extended properties overview for more information about when to use open extensions or extended properties, and how to specify extended properties.
š” TIP: CRITICAL: Do not try to guess the email address of the recipients. Use the list-users tool to find the email address of the recipients.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| body | Yes | ||
| includeHeaders | No | Include response headers (including ETag) in the response metadata | |
| excludeResponse | No | Exclude the full response body and only return success or failure indication |
Implementation Reference
- src/graph-tools.ts:86-369 (handler)The 'executeGraphTool' function acts as the generic handler for all registered graph tools, including 'create-calendar-event'. It parses tool-specific configurations (like URL encoding overrides, headers, and body handling), prepares the API request, handles authentication-wrapped responses, and executes the request using the provided graphClient.
async function executeGraphTool( tool: (typeof api.endpoints)[0], config: EndpointConfig | undefined, graphClient: GraphClient, params: Record<string, unknown> ): Promise<CallToolResult> { logger.info(`Tool ${tool.alias} called with params: ${JSON.stringify(params)}`); try { const parameterDefinitions = tool.parameters || []; let path = tool.path; const queryParams: Record<string, string> = {}; const headers: Record<string, string> = {}; let body: unknown = null; for (const [paramName, paramValue] of Object.entries(params)) { // Skip control parameters - not part of the Microsoft Graph API if ( [ 'fetchAllPages', 'includeHeaders', 'excludeResponse', 'timezone', 'expandExtendedProperties', ].includes(paramName) ) { continue; } // Ok, so, MCP clients (such as claude code) doesn't support $ in parameter names, // and others might not support __, so we strip them in hack.ts and restore them here const odataParams = [ 'filter', 'select', 'expand', 'orderby', 'skip', 'top', 'count', 'search', 'format', ]; // Handle both "top" and "$top" formats - strip $ if present, then re-add it const normalizedParamName = paramName.startsWith('$') ? paramName.slice(1) : paramName; const isOdataParam = odataParams.includes(normalizedParamName.toLowerCase()); const fixedParamName = isOdataParam ? `$${normalizedParamName.toLowerCase()}` : paramName; // Look up param definition using normalized name (without $) for OData params const paramDef = parameterDefinitions.find( (p) => p.name === paramName || (isOdataParam && p.name === normalizedParamName) ); if (paramDef) { switch (paramDef.type) { case 'Path': { // Check if this parameter should skip URL encoding (for function-style API calls) const shouldSkipEncoding = config?.skipEncoding?.includes(paramName) ?? false; const encodedValue = shouldSkipEncoding ? (paramValue as string) : encodeURIComponent(paramValue as string); path = path .replace(`{${paramName}}`, encodedValue) .replace(`:${paramName}`, encodedValue); break; } case 'Query': if (paramValue !== '' && paramValue != null) { queryParams[fixedParamName] = `${paramValue}`; } break; case 'Body': if (paramDef.schema) { const parseResult = paramDef.schema.safeParse(paramValue); if (!parseResult.success) { const wrapped = { [paramName]: paramValue }; const wrappedResult = paramDef.schema.safeParse(wrapped); if (wrappedResult.success) { logger.info( `Auto-corrected parameter '${paramName}': AI passed nested field directly, wrapped it as {${paramName}: ...}` ); body = wrapped; } else { body = paramValue; } } else { body = paramValue; } } else { body = paramValue; } break; case 'Header': headers[fixedParamName] = `${paramValue}`; break; } } else if (paramName === 'body') { body = paramValue; logger.info(`Set body param: ${JSON.stringify(body)}`); } } // Handle timezone parameter for calendar endpoints if (config?.supportsTimezone && params.timezone) { headers['Prefer'] = `outlook.timezone="${params.timezone}"`; logger.info(`Setting timezone header: Prefer: outlook.timezone="${params.timezone}"`); } // Handle expandExtendedProperties parameter for calendar endpoints if (config?.supportsExpandExtendedProperties && params.expandExtendedProperties === true) { const expandValue = 'singleValueExtendedProperties'; if (queryParams['$expand']) { queryParams['$expand'] += `,${expandValue}`; } else { queryParams['$expand'] = expandValue; } logger.info(`Adding $expand=${expandValue} for extended properties`); } if (config?.contentType) { headers['Content-Type'] = config.contentType; logger.info(`Setting custom Content-Type: ${config.contentType}`); } if (Object.keys(queryParams).length > 0) { const queryString = Object.entries(queryParams) .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join('&'); path = `${path}${path.includes('?') ? '&' : '?'}${queryString}`; } const options: { method: string; headers: Record<string, string>; body?: string; rawResponse?: boolean; includeHeaders?: boolean; excludeResponse?: boolean; queryParams?: Record<string, string>; } = { method: tool.method.toUpperCase(), headers, }; if (options.method !== 'GET' && body) { if (config?.contentType === 'text/html') { if (typeof body === 'string') { options.body = body; } else if (typeof body === 'object' && 'content' in body) { options.body = (body as { content: string }).content; } else { options.body = String(body); } } else { options.body = typeof body === 'string' ? body : JSON.stringify(body); } } const isProbablyMediaContent = tool.errors?.some((error) => error.description === 'Retrieved media content') || path.endsWith('/content'); if (config?.returnDownloadUrl && path.endsWith('/content')) { path = path.replace(/\/content$/, ''); logger.info( `Auto-returning download URL for ${tool.alias} (returnDownloadUrl=true in endpoints.json)` ); } else if (isProbablyMediaContent) { options.rawResponse = true; } // Set includeHeaders if requested if (params.includeHeaders === true) { options.includeHeaders = true; } // Set excludeResponse if requested if (params.excludeResponse === true) { options.excludeResponse = true; } logger.info(`Making graph request to ${path} with options: ${JSON.stringify(options)}`); let response = await graphClient.graphRequest(path, options); const fetchAllPages = params.fetchAllPages === true; if (fetchAllPages && response?.content?.[0]?.text) { try { let combinedResponse = JSON.parse(response.content[0].text); let allItems = combinedResponse.value || []; let nextLink = combinedResponse['@odata.nextLink']; let pageCount = 1; while (nextLink && pageCount < 100) { logger.info(`Fetching page ${pageCount + 1} from: ${nextLink}`); const url = new URL(nextLink); const nextPath = url.pathname.replace('/v1.0', ''); const nextOptions = { ...options }; const nextQueryParams: Record<string, string> = {}; for (const [key, value] of url.searchParams.entries()) { nextQueryParams[key] = value; } nextOptions.queryParams = nextQueryParams; const nextResponse = await graphClient.graphRequest(nextPath, nextOptions); if (nextResponse?.content?.[0]?.text) { const nextJsonResponse = JSON.parse(nextResponse.content[0].text); if (nextJsonResponse.value && Array.isArray(nextJsonResponse.value)) { allItems = allItems.concat(nextJsonResponse.value); } nextLink = nextJsonResponse['@odata.nextLink']; pageCount++; } else { break; } } if (pageCount >= 100) { logger.warn(`Reached maximum page limit (100) for pagination`); } combinedResponse.value = allItems; if (combinedResponse['@odata.count']) { combinedResponse['@odata.count'] = allItems.length; } delete combinedResponse['@odata.nextLink']; response.content[0].text = JSON.stringify(combinedResponse); logger.info( `Pagination complete: collected ${allItems.length} items across ${pageCount} pages` ); } catch (e) { logger.error(`Error during pagination: ${e}`); } } if (response?.content?.[0]?.text) { const responseText = response.content[0].text; logger.info(`Response size: ${responseText.length} characters`); try { const jsonResponse = JSON.parse(responseText); if (jsonResponse.value && Array.isArray(jsonResponse.value)) { logger.info(`Response contains ${jsonResponse.value.length} items`); } if (jsonResponse['@odata.nextLink']) { logger.info(`Response has pagination nextLink: ${jsonResponse['@odata.nextLink']}`); } } catch { // Non-JSON response } } // Convert McpResponse to CallToolResult with the correct structure const content: ContentItem[] = response.content.map((item) => ({ type: 'text' as const, text: item.text, })); return { content, _meta: response._meta, isError: response.isError, }; } catch (error) { logger.error(`Error in tool ${tool.alias}: ${(error as Error).message}`); return { content: [ { type: 'text', text: JSON.stringify({ error: `Error in tool ${tool.alias}: ${(error as Error).message}`, }), }, ], isError: true, }; } } - src/graph-tools.ts:466-477 (registration)This is where the individual tools, including 'create-calendar-event', are registered with the MCP server. It iterates over the generated API definitions ('api.endpoints'), builds Zod schemas for the parameters, and assigns 'executeGraphTool' as the handler for each tool.
server.tool( tool.alias, toolDescription, paramSchema, { title: tool.alias, readOnlyHint: tool.method.toUpperCase() === 'GET', destructiveHint: ['POST', 'PATCH', 'DELETE'].includes(tool.method.toUpperCase()), openWorldHint: true, // All tools call Microsoft Graph API }, async (params) => executeGraphTool(tool, endpointConfig, graphClient, params) );