Skip to main content
Glama

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

TableJSON Schema
NameRequiredDescriptionDefault
bodyYes
includeHeadersNoInclude response headers (including ETag) in the response metadata
excludeResponseNoExclude the full response body and only return success or failure indication

Implementation Reference

  • 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,
        };
      }
    }
  • 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)
    );
Install Server

Other Tools

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/alfredo-ia/ms-365-mcp-server'

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