Skip to main content
Glama

list-events

Read-only

Retrieve upcoming events from your Outlook calendar. Optionally specify the number of events to return, up to 50.

Instructions

Lists upcoming events from your calendar

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
countNoNumber of events to retrieve (default: 10, max: 50)

Implementation Reference

  • Registration of the 'list-events' tool with its metadata, input schema, and handler reference.
    {
      name: 'list-events',
      description: 'Lists upcoming events from your calendar',
      annotations: {
        title: 'List Calendar Events',
        readOnlyHint: true,
        openWorldHint: false,
      },
      inputSchema: {
        type: 'object',
        properties: {
          count: {
            type: 'number',
            description: 'Number of events to retrieve (default: 10, max: 50)',
          },
        },
        additionalProperties: false,
        required: [],
      },
      handler: handleListEvents,
    },
  • Handler function that calls the Microsoft Graph API (me/events), formats and returns upcoming calendar events.
    async function handleListEvents(args) {
      const count = Math.min(args.count || 10, config.MAX_RESULT_COUNT);
    
      try {
        // Get access token
        const accessToken = await ensureAuthenticated();
    
        // Build API endpoint
        const endpoint = 'me/events';
    
        // Add query parameters
        const queryParams = {
          $top: count,
          $orderby: 'start/dateTime',
          $filter: `start/dateTime ge '${new Date().toISOString()}'`,
          $select: config.CALENDAR_SELECT_FIELDS,
        };
    
        // Make API call
        const response = await callGraphAPI(
          accessToken,
          'GET',
          endpoint,
          null,
          queryParams
        );
    
        if (!response.value || response.value.length === 0) {
          return {
            content: [
              {
                type: 'text',
                text: 'No calendar events found.',
              },
            ],
          };
        }
    
        // Format results
        const tz = config.DEFAULT_TIMEZONE;
        const eventList = response.value
          .map((event, index) => {
            const startDt = event.start.dateTime.endsWith('Z')
              ? event.start.dateTime
              : `${event.start.dateTime}Z`;
            const endDt = event.end.dateTime.endsWith('Z')
              ? event.end.dateTime
              : `${event.end.dateTime}Z`;
            const startDate = new Date(startDt).toLocaleString('en-AU', {
              timeZone: tz,
              dateStyle: 'medium',
              timeStyle: 'short',
            });
            const endDate = new Date(endDt).toLocaleString('en-AU', {
              timeZone: tz,
              dateStyle: 'medium',
              timeStyle: 'short',
            });
            const location = event.location.displayName || 'No location';
    
            return `${index + 1}. ${event.subject} - Location: ${location}\nStart: ${startDate}\nEnd: ${endDate}\nSummary: ${event.bodyPreview}\nID: ${event.id}\n`;
          })
          .join('\n');
    
        return {
          content: [
            {
              type: 'text',
              text: `Found ${response.value.length} events:\n\n${eventList}`,
            },
          ],
        };
      } catch (error) {
        if (error.message === 'Authentication required') {
          return {
            content: [
              {
                type: 'text',
                text: "Authentication required. Please use the 'authenticate' tool first.",
              },
            ],
          };
        }
    
        return {
          content: [
            {
              type: 'text',
              text: `Error listing events: ${error.message}`,
            },
          ],
        };
      }
    }
  • Input schema for list-events: optional 'count' parameter (number, default 10, max 50). No required parameters.
    inputSchema: {
      type: 'object',
      properties: {
        count: {
          type: 'number',
          description: 'Number of events to retrieve (default: 10, max: 50)',
        },
      },
      additionalProperties: false,
      required: [],
    },
  • Configuration constant CALENDAR_SELECT_FIELDS used by the handler to specify which fields to retrieve from the Graph API.
      CALENDAR_SELECT_FIELDS:
        'id,subject,bodyPreview,start,end,location,organizer,attendees,isAllDay,isCancelled',
    
      // Email field presets (use getEmailFields() for dynamic selection)
      FIELD_PRESETS,
      getEmailFields,
    
      // Legacy email fields (kept for backward compatibility)
      EMAIL_SELECT_FIELDS: getEmailFields('list'),
      EMAIL_DETAIL_FIELDS: getEmailFields('read'),
      EMAIL_FORENSIC_FIELDS: getEmailFields('forensic'),
      EMAIL_EXPORT_FIELDS: getEmailFields('export'),
    
      // Folder field presets
      FOLDER_FIELDS,
      getFolderFields,
    
      // Verbosity levels for response formatting
      VERBOSITY,
    
      // Default limits for token efficiency
      DEFAULT_LIMITS,
    
      // Pagination (updated to use DEFAULT_LIMITS)
      DEFAULT_PAGE_SIZE: DEFAULT_LIMITS.listEmails,
      MAX_RESULT_COUNT: 100, // Increased for batch operations
    
      // Search defaults (reduced for token efficiency)
      DEFAULT_SEARCH_RESULTS: DEFAULT_LIMITS.searchEmails,
    
      // Immutable IDs (opt-in: IDs persist through folder moves)
      USE_IMMUTABLE_IDS: process.env.OUTLOOK_IMMUTABLE_IDS === 'true',
    
      // Timezone
      DEFAULT_TIMEZONE: 'Australia/Melbourne', // Updated for Nathan's timezone
    };
  • The callGraphAPI helper function that makes HTTP requests to the Microsoft Graph API, used by the list-events handler.
    async function callGraphAPI(
      accessToken,
      method,
      path,
      data = null,
      queryParams = {},
      extraHeaders = {}
    ) {
      // For test tokens, we'll simulate the API call
      if (config.USE_TEST_MODE && accessToken.startsWith('test_access_token_')) {
        return mockData.simulateGraphAPIResponse(method, path, data, queryParams);
      }
    
      try {
        // Check if path already contains the full URL (from nextLink)
        let finalUrl;
        if (path.startsWith('http://') || path.startsWith('https://')) {
          // Path is already a full URL (from pagination nextLink)
          finalUrl = path;
        } else {
          // Build URL from path and queryParams
          // Encode path segments properly
          const encodedPath = path
            .split('/')
            .map((segment) => encodeURIComponent(segment))
            .join('/');
    
          // Build query string from parameters with special handling for OData filters
          let queryString = '';
          if (Object.keys(queryParams).length > 0) {
            // Handle $filter parameter specially to ensure proper URI encoding
            const filter = queryParams.$filter;
            if (filter) {
              delete queryParams.$filter; // Remove from regular params
            }
    
            // Build query string with proper encoding for regular params
            const params = new URLSearchParams();
            for (const [key, value] of Object.entries(queryParams)) {
              params.append(key, value);
            }
    
            queryString = params.toString();
    
            // Add filter parameter separately with proper encoding
            if (filter) {
              if (queryString) {
                queryString += `&$filter=${encodeURIComponent(filter)}`;
              } else {
                queryString = `$filter=${encodeURIComponent(filter)}`;
              }
            }
    
            if (queryString) {
              queryString = `?${queryString}`;
            }
          }
    
          finalUrl = `${config.GRAPH_API_ENDPOINT}${encodedPath}${queryString}`;
        }
    
        return new Promise((resolve, reject) => {
          const headers = {
            Authorization: `Bearer ${accessToken}`,
            'Content-Type': 'application/json',
          };
    
          // Add immutable IDs header when enabled globally
          if (config.USE_IMMUTABLE_IDS) {
            headers.Prefer = 'IdType="ImmutableId"';
          }
    
          // Merge any extra headers (caller overrides take precedence)
          Object.assign(headers, extraHeaders);
    
          const options = {
            method: method,
            headers,
          };
    
          const req = https.request(finalUrl, options, (res) => {
            let responseData = '';
    
            res.on('data', (chunk) => {
              responseData += chunk;
            });
    
            res.on('end', () => {
              if (res.statusCode >= 200 && res.statusCode < 300) {
                try {
                  responseData = responseData ? responseData : '{}';
                  const jsonResponse = JSON.parse(responseData);
                  resolve(jsonResponse);
                } catch (error) {
                  reject(new Error(`Error parsing API response: ${error.message}`));
                }
              } else if (res.statusCode === 401) {
                // Token expired or invalid
                reject(new Error('UNAUTHORIZED'));
              } else {
                // Truncate response to avoid leaking sensitive data in error messages
                const safeResponse = responseData.substring(0, 200);
                reject(
                  new Error(
                    `API call failed with status ${res.statusCode}: ${safeResponse}`
                  )
                );
              }
            });
          });
    
          req.on('error', (error) => {
            reject(new Error(`Network error during API call: ${error.message}`));
          });
    
          if (
            data &&
            (method === 'POST' || method === 'PATCH' || method === 'PUT')
          ) {
            req.write(JSON.stringify(data));
          }
    
          req.end();
        });
      } catch (error) {
        console.error('Error calling Graph API:', error);
        throw error;
      }
    }
    
    /**
     * Calls Graph API with pagination support to retrieve all results up to maxCount
     * @param {string} accessToken - The access token for authentication
     * @param {string} method - HTTP method (GET only for pagination)
     * @param {string} path - API endpoint path
     * @param {object} queryParams - Initial query parameters
     * @param {number} maxCount - Maximum number of items to retrieve (0 = all)
     * @returns {Promise<object>} - Combined API response with all items
     * @throws {Error} If method is not 'GET'
     * @throws {Error} If any page request fails for any other reason
     */
    async function callGraphAPIPaginated(
      accessToken,
      method,
      path,
      queryParams = {},
      maxCount = 0
    ) {
      if (method !== 'GET') {
        throw new Error('Pagination only supports GET requests');
      }
    
      const allItems = [];
      let nextLink;
      let currentUrl = path;
      let currentParams = { ...queryParams };
    
      try {
        do {
          // Make API call
          const response = await callGraphAPI(
            accessToken,
            method,
            currentUrl,
            null,
            currentParams
          );
    
          // Add items from this page
          if (response.value && Array.isArray(response.value)) {
            allItems.push(...response.value);
          }
    
          // Check if we've reached the desired count
          if (maxCount > 0 && allItems.length >= maxCount) {
            break;
          }
    
          // Get next page URL
          nextLink = response['@odata.nextLink'];
    
          if (nextLink) {
            // Pass the full nextLink URL directly to callGraphAPI
            currentUrl = nextLink;
            currentParams = {}; // nextLink already contains all params
          }
        } while (nextLink);
    
        // Trim to exact count if needed
        const finalItems = maxCount > 0 ? allItems.slice(0, maxCount) : allItems;
    
        return {
          value: finalItems,
          '@odata.count': finalItems.length,
        };
      } catch (error) {
        console.error('Error during pagination:', error);
        throw error;
      }
    }
    
    /**
     * Sends multiple Graph API requests in a single batch call ($batch).
     * Supports up to 20 requests per batch (Graph API limit).
     * @param {string} accessToken - The access token for authentication
     * @param {Array<{id: string, method: string, url: string, body?: object, headers?: object}>} requests - Batch requests
     * @returns {Promise<Array<{id: string, status: number, body: object}>>} - Array of responses
     */
    async function callGraphAPIBatch(accessToken, requests) {
      if (!Array.isArray(requests) || requests.length === 0) {
        throw new Error('Batch requests must be a non-empty array');
      }
    
      if (requests.length > 20) {
        throw new Error('Batch requests cannot exceed 20 (Graph API limit)');
      }
    
      // Test mode
      if (config.USE_TEST_MODE && accessToken.startsWith('test_access_token_')) {
        return requests.map((req) => ({
          id: req.id,
          status: 200,
          body: mockData.simulateGraphAPIResponse(
            req.method,
            req.url,
            req.body || null,
            {}
          ),
        }));
      }
    
      const batchPayload = {
        requests: requests.map((req) => ({
          id: req.id,
          method: req.method,
          url: req.url.startsWith('/') ? req.url : `/${req.url}`,
          ...(req.body && { body: req.body }),
          ...(req.headers && { headers: req.headers }),
        })),
      };
    
      const response = await callGraphAPI(
        accessToken,
        'POST',
        '$batch',
        batchPayload
      );
    
      return (response.responses || []).sort(
        (a, b) => parseInt(a.id) - parseInt(b.id)
      );
    }
    
    /**
     * Calls Graph API to get raw MIME content (for email export)
     * In test mode (USE_TEST_MODE=true), returns mock MIME content instead of calling the real API.
     * @param {string} accessToken - The access token for authentication
     * @param {string} emailId - The email ID to export
     * @returns {Promise<string>} - Raw MIME content as string
     * @throws {Error} 'UNAUTHORIZED' if the server returns HTTP 401 (token expired or invalid)
     * @throws {Error} If the HTTP status is outside 2xx or a network error occurs
     */
    async function callGraphAPIRaw(accessToken, emailId) {
      // Test mode: return mock MIME content
      if (config.USE_TEST_MODE && accessToken.startsWith('test_access_token_')) {
        return mockData.getMockMimeContent
          ? mockData.getMockMimeContent(emailId)
          : `MIME-Version: 1.0\nContent-Type: text/plain\n\nTest email content for ${emailId}`;
      }
    
      return new Promise((resolve, reject) => {
        const path = `me/messages/${encodeURIComponent(emailId)}/$value`;
        const finalUrl = `${config.GRAPH_API_ENDPOINT}${path}`;
    
        const options = {
          method: 'GET',
          headers: {
            Authorization: `Bearer ${accessToken}`,
            Accept: 'message/rfc822', // Request MIME format
          },
        };
    
        const req = https.request(finalUrl, options, (res) => {
          let responseData = '';
    
          // Collect data as UTF-8 string
          res.setEncoding('utf8');
    
          res.on('data', (chunk) => {
            responseData += chunk;
          });
    
          res.on('end', () => {
            if (res.statusCode >= 200 && res.statusCode < 300) {
              resolve(responseData);
            } else if (res.statusCode === 401) {
              reject(new Error('UNAUTHORIZED'));
            } else {
              reject(
                new Error(
                  `MIME export failed with status ${res.statusCode}: ${responseData.substring(0, 200)}`
                )
              );
            }
          });
        });
    
        req.on('error', (error) => {
          reject(new Error(`Network error during MIME export: ${error.message}`));
        });
    
        req.end();
      });
    }
    
    /**
     * Calls Graph API with automatic auth and 401 retry.
     * Gets token via ensureAuthenticated(), and if a 401 occurs,
     * refreshes the token and retries once.
     * @param {string} method - HTTP method
     * @param {string} path - API endpoint path
     * @param {object} data - Request body
     * @param {object} queryParams - Query parameters
     * @param {object} extraHeaders - Additional headers
     * @returns {Promise<object>} - API response
     */
    async function callGraphAPIWithAuth(
      method,
      path,
      data = null,
      queryParams = {},
      extraHeaders = {}
    ) {
      // Lazy require to avoid circular dependency
      const { ensureAuthenticated, tokenStorage } = require('../auth');
    
      const accessToken = await ensureAuthenticated();
      try {
        return await callGraphAPI(
          accessToken,
          method,
          path,
          data,
          queryParams,
          extraHeaders
        );
      } catch (error) {
        if (error.message === 'UNAUTHORIZED' && tokenStorage) {
          console.error('[GRAPH-API] 401 received, attempting token refresh...');
          try {
            const newToken = await tokenStorage.refreshAccessToken();
            if (newToken) {
              return await callGraphAPI(
                newToken,
                method,
                path,
                data,
                queryParams,
                extraHeaders
              );
            }
          } catch (refreshError) {
            console.error(
              '[GRAPH-API] Token refresh failed:',
              refreshError.message
            );
          }
        }
        throw error;
      }
    }
    
    module.exports = {
      callGraphAPI,
      callGraphAPIPaginated,
      callGraphAPIBatch,
      callGraphAPIRaw,
      callGraphAPIWithAuth,
    };
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true; description adds minimal info about 'upcoming' events but does not define time range or pagination behavior.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Single, concise sentence front-loads key information with no extraneous words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Adequate for a simple tool with good annotations, but lacks output format details and does not clarify the definition of 'upcoming'.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema covers 100% of parameters; description adds no additional meaning beyond the schema's description for the 'count' parameter.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Description clearly states 'Lists upcoming events from your calendar', specifying the action and resource, and distinguishes from sibling tools like create-event and manage-event.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance on when to use this tool versus alternatives; no exclusionary criteria or recommended context provided.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

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/littlebearapps/outlook-mcp'

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