outlook-mcp/utils/odata-helpers.js
Seton Carmichael a7886b5b2b Initial commit: Outlook MCP Server v1.0.0
MCP server providing Microsoft Outlook integration via Graph API:
- Email: list, search, read, send, thread reconstruction
- Calendar: list, create, decline, cancel, delete events
- Folders: list, create, move emails
- Inbox rules: list, create, reorder
- MSAL device code flow auth with persistent token cache
- Test mode with mock data
- Comprehensive README with setup, config, and tool reference

20 MCP tools across 5 modules. Node.js >= 14. MIT license.
2026-06-21 19:39:31 -04:00

221 lines
5.9 KiB
JavaScript

/**
* OData helper functions for Microsoft Graph API
*/
/**
* Escapes a string for use in OData queries
* @param {string} str - The string to escape
* @returns {string} - The escaped string
*/
function escapeODataString(str) {
if (!str) return str;
// Replace single quotes with double single quotes (OData escaping)
// And remove any special characters that could cause OData syntax errors
str = str.replace(/'/g, "''");
// Escape other potentially problematic characters
str = str.replace(/[\(\)\{\}\[\]\:\;\,\/\?\&\=\+\*\%\$\#\@\!\^]/g, '');
console.error(`Escaped OData string: '${str}'`);
return str;
}
/**
* Builds an OData filter from filter conditions
* @param {Array<string>} conditions - Array of filter conditions
* @returns {string} - Combined OData filter expression
*/
function buildODataFilter(conditions) {
if (!conditions || conditions.length === 0) {
return '';
}
return conditions.join(' and ');
}
/**
* Gets start of day for a given date
* @param {Date} date - The date
* @returns {Date} - Start of day
*/
function startOfDay(date) {
const start = new Date(date);
start.setHours(0, 0, 0, 0);
return start;
}
/**
* Gets end of day for a given date
* @param {Date} date - The date
* @returns {Date} - End of day
*/
function endOfDay(date) {
const end = new Date(date);
end.setHours(23, 59, 59, 999);
return end;
}
/**
* Parses date input (ISO string or relative date)
* @param {string} dateInput - Date string
* @returns {Date} - Parsed date
*/
function parseDate(dateInput) {
if (!dateInput) return null;
const now = new Date();
const today = new Date(now);
// Handle relative dates
const relativeMap = {
'today': today,
'yesterday': new Date(now.getTime() - 24*60*60*1000),
'tomorrow': new Date(now.getTime() + 24*60*60*1000),
'last7days': new Date(now.getTime() - 7*24*60*60*1000),
'last30days': new Date(now.getTime() - 30*24*60*60*1000),
'last90days': new Date(now.getTime() - 90*24*60*60*1000)
};
if (relativeMap[dateInput.toLowerCase()]) {
return relativeMap[dateInput.toLowerCase()];
}
// Handle ISO dates
const parsed = new Date(dateInput);
if (isNaN(parsed.getTime())) {
throw new Error(`Invalid date format: ${dateInput}`);
}
return parsed;
}
/**
* Processes predefined date ranges
* @param {string} dateRange - Predefined range
* @returns {Object} - Object with from and to dates
*/
function processDateRange(dateRange) {
if (!dateRange) return null;
const now = new Date();
const today = new Date(now);
switch (dateRange.toLowerCase()) {
case 'today':
return {
from: startOfDay(today),
to: endOfDay(today)
};
case 'yesterday': {
const yesterday = new Date(now.getTime() - 24*60*60*1000);
return {
from: startOfDay(yesterday),
to: endOfDay(yesterday)
};
}
case 'last7days':
return {
from: new Date(now.getTime() - 7*24*60*60*1000),
to: now
};
case 'last30days':
return {
from: new Date(now.getTime() - 30*24*60*60*1000),
to: now
};
case 'thisweek': {
const startOfWeek = new Date(today);
startOfWeek.setDate(today.getDate() - today.getDay());
return {
from: startOfDay(startOfWeek),
to: endOfDay(today)
};
}
case 'lastweek': {
const startOfLastWeek = new Date(today);
startOfLastWeek.setDate(today.getDate() - today.getDay() - 7);
const endOfLastWeek = new Date(startOfLastWeek);
endOfLastWeek.setDate(startOfLastWeek.getDate() + 6);
return {
from: startOfDay(startOfLastWeek),
to: endOfDay(endOfLastWeek)
};
}
case 'thismonth': {
const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
return {
from: startOfDay(startOfMonth),
to: endOfDay(today)
};
}
case 'lastmonth': {
const startOfLastMonth = new Date(today.getFullYear(), today.getMonth() - 1, 1);
const endOfLastMonth = new Date(today.getFullYear(), today.getMonth(), 0);
return {
from: startOfDay(startOfLastMonth),
to: endOfDay(endOfLastMonth)
};
}
default:
throw new Error(`Unknown date range: ${dateRange}`);
}
}
/**
* Builds date filter conditions for OData queries
* @param {string} dateFrom - Start date
* @param {string} dateTo - End date
* @param {string} dateRange - Predefined range
* @returns {Array<string>} - Array of filter conditions
*/
function buildDateFilter(dateFrom, dateTo, dateRange) {
const conditions = [];
try {
if (dateRange) {
const range = processDateRange(dateRange);
if (range) {
conditions.push(`receivedDateTime ge ${range.from.toISOString()}`);
conditions.push(`receivedDateTime le ${range.to.toISOString()}`);
}
} else {
if (dateFrom) {
const fromDate = parseDate(dateFrom);
conditions.push(`receivedDateTime ge ${fromDate.toISOString()}`);
}
if (dateTo) {
const toDate = parseDate(dateTo);
// If only date provided (no time), set to end of day
if (dateTo.length === 10) { // YYYY-MM-DD format
conditions.push(`receivedDateTime le ${endOfDay(toDate).toISOString()}`);
} else {
conditions.push(`receivedDateTime le ${toDate.toISOString()}`);
}
}
}
} catch (error) {
console.error(`Date filter error: ${error.message}`);
// Return empty conditions on error to avoid breaking the query
}
return conditions;
}
module.exports = {
escapeODataString,
buildODataFilter,
parseDate,
processDateRange,
buildDateFilter,
startOfDay,
endOfDay
};