fix(outlook-mcp): P0 audit fixes

- Handle empty 2xx Graph API responses without JSON parse errors
- Fix list-rules handler import (handleListRules named export)
- Add timezone-aware display formatting using MS_TIMEZONE / DEFAULT_TIMEZONE
- Return created event ID in create-event success message
- Change default timezone from Windows name to IANA (America/New_York)
This commit is contained in:
Seton Carmichael 2026-06-21 20:28:11 -04:00
parent ba45f57b39
commit 68a64461eb
11 changed files with 71 additions and 18 deletions

View file

@ -44,7 +44,7 @@ async function handleCreateEvent(args) {
return { return {
content: [{ content: [{
type: "text", type: "text",
text: `Event '${subject}' has been successfully created.` text: `Event '${subject}' has been successfully created. ID: ${response.id || '(not returned)'}`
}] }]
}; };
} catch (error) { } catch (error) {

View file

@ -1,3 +1,5 @@
const { formatDateTime } = require('../utils/time-formatter');
/** /**
* List events functionality * List events functionality
*/ */
@ -59,8 +61,8 @@ async function handleListEvents(args) {
// Format results // Format results
const eventList = response.value.map((event, index) => { const eventList = response.value.map((event, index) => {
const startDate = new Date(event.start.dateTime).toLocaleString(event.start.timeZone); const startDate = formatDateTime(event.start.dateTime, event.start.timeZone);
const endDate = new Date(event.end.dateTime).toLocaleString(event.end.timeZone); const endDate = formatDateTime(event.end.dateTime, event.end.timeZone);
const location = event.location.displayName || 'No location'; const location = event.location.displayName || 'No location';
return `${index + 1}. ${event.subject} - Location: ${location}\nStart: ${startDate}\nEnd: ${endDate}\nSubject: ${event.subject}\nSummary: ${event.bodyPreview}\nID: ${event.id}\n`; return `${index + 1}. ${event.subject} - Location: ${location}\nStart: ${startDate}\nEnd: ${endDate}\nSubject: ${event.subject}\nSummary: ${event.bodyPreview}\nID: ${event.id}\n`;

View file

@ -43,9 +43,9 @@ module.exports = {
// Calendar constants // Calendar constants
CALENDAR_SELECT_FIELDS: 'id,subject,bodyPreview,start,end,location,organizer,attendees,isAllDay,isCancelled', CALENDAR_SELECT_FIELDS: 'id,subject,bodyPreview,start,end,location,organizer,attendees,isAllDay,isCancelled',
// Default timezone for calendar event creation (IANA tz name, e.g. 'America/New_York'). // Default timezone for calendar event creation and display (IANA tz name, e.g. 'America/New_York').
// Override via MS_TIMEZONE env var. Graph API accepts IANA timezone identifiers. // Override via MS_TIMEZONE env var. Graph API accepts IANA timezone identifiers.
DEFAULT_TIMEZONE: process.env.MS_TIMEZONE || 'Eastern Standard Time', DEFAULT_TIMEZONE: process.env.MS_TIMEZONE || 'America/New_York',
DEFAULT_PAGE_SIZE: 25, DEFAULT_PAGE_SIZE: 25,
MAX_RESULT_COUNT: 500 MAX_RESULT_COUNT: 500
}; };

View file

@ -1,3 +1,5 @@
const { formatDateTime } = require('../utils/time-formatter');
/** /**
* List emails functionality * List emails functionality
*/ */
@ -51,7 +53,7 @@ async function handleListEmails(args) {
// Format results // Format results
const emailList = response.value.map((email, index) => { const emailList = response.value.map((email, index) => {
const sender = email.from ? email.from.emailAddress : { name: 'Unknown', address: 'unknown' }; const sender = email.from ? email.from.emailAddress : { name: 'Unknown', address: 'unknown' };
const date = new Date(email.receivedDateTime).toLocaleString(); const date = formatDateTime(email.receivedDateTime);
const readStatus = email.isRead ? '' : '[UNREAD] '; const readStatus = email.isRead ? '' : '[UNREAD] ';
const convLine = email.conversationId ? `ConversationID: ${email.conversationId}\n` : ''; const convLine = email.conversationId ? `ConversationID: ${email.conversationId}\n` : '';

View file

@ -5,6 +5,7 @@ const config = require('../config');
const { callGraphAPI } = require('../utils/graph-api'); const { callGraphAPI } = require('../utils/graph-api');
const { ensureAuthenticated } = require('../auth'); const { ensureAuthenticated } = require('../auth');
const { cleanBody } = require('../utils/bodyParser'); const { cleanBody } = require('../utils/bodyParser');
const { formatDateTime } = require('../utils/time-formatter');
/** /**
* Format a single email for display * Format a single email for display
@ -23,7 +24,7 @@ function formatEmail(email, emailId) {
const to = email.toRecipients ? email.toRecipients.map(r => `${r.emailAddress.name} (${r.emailAddress.address})`).join(", ") : 'None'; const to = email.toRecipients ? email.toRecipients.map(r => `${r.emailAddress.name} (${r.emailAddress.address})`).join(", ") : 'None';
const cc = email.ccRecipients && email.ccRecipients.length > 0 ? email.ccRecipients.map(r => `${r.emailAddress.name} (${r.emailAddress.address})`).join(", ") : 'None'; const cc = email.ccRecipients && email.ccRecipients.length > 0 ? email.ccRecipients.map(r => `${r.emailAddress.name} (${r.emailAddress.address})`).join(", ") : 'None';
const bcc = email.bccRecipients && email.bccRecipients.length > 0 ? email.bccRecipients.map(r => `${r.emailAddress.name} (${r.emailAddress.address})`).join(", ") : 'None'; const bcc = email.bccRecipients && email.bccRecipients.length > 0 ? email.bccRecipients.map(r => `${r.emailAddress.name} (${r.emailAddress.address})`).join(", ") : 'None';
const date = new Date(email.receivedDateTime).toLocaleString(); const date = formatDateTime(email.receivedDateTime);
// Extract and clean body content (cleanBody handles both HTML and plain text) // Extract and clean body content (cleanBody handles both HTML and plain text)
let body = ''; let body = '';

View file

@ -5,6 +5,7 @@ const config = require('../config');
const { callGraphAPI } = require('../utils/graph-api'); const { callGraphAPI } = require('../utils/graph-api');
const { ensureAuthenticated } = require('../auth'); const { ensureAuthenticated } = require('../auth');
const { cleanBody } = require('../utils/bodyParser'); const { cleanBody } = require('../utils/bodyParser');
const { formatDateTime } = require('../utils/time-formatter');
/** /**
* Read email handler * Read email handler
@ -52,7 +53,7 @@ async function handleReadEmail(args) {
const to = email.toRecipients ? email.toRecipients.map(r => `${r.emailAddress.name} (${r.emailAddress.address})`).join(", ") : 'None'; const to = email.toRecipients ? email.toRecipients.map(r => `${r.emailAddress.name} (${r.emailAddress.address})`).join(", ") : 'None';
const cc = email.ccRecipients && email.ccRecipients.length > 0 ? email.ccRecipients.map(r => `${r.emailAddress.name} (${r.emailAddress.address})`).join(", ") : 'None'; const cc = email.ccRecipients && email.ccRecipients.length > 0 ? email.ccRecipients.map(r => `${r.emailAddress.name} (${r.emailAddress.address})`).join(", ") : 'None';
const bcc = email.bccRecipients && email.bccRecipients.length > 0 ? email.bccRecipients.map(r => `${r.emailAddress.name} (${r.emailAddress.address})`).join(", ") : 'None'; const bcc = email.bccRecipients && email.bccRecipients.length > 0 ? email.bccRecipients.map(r => `${r.emailAddress.name} (${r.emailAddress.address})`).join(", ") : 'None';
const date = new Date(email.receivedDateTime).toLocaleString(); const date = formatDateTime(email.receivedDateTime);
// Extract and clean body content (cleanBody handles both HTML and plain text) // Extract and clean body content (cleanBody handles both HTML and plain text)
let body = ''; let body = '';

View file

@ -5,6 +5,7 @@ const config = require('../config');
const { callGraphAPI } = require('../utils/graph-api'); const { callGraphAPI } = require('../utils/graph-api');
const { ensureAuthenticated } = require('../auth'); const { ensureAuthenticated } = require('../auth');
const { resolveFolderPath } = require('./folder-utils'); const { resolveFolderPath } = require('./folder-utils');
const { formatDateTime } = require('../utils/time-formatter');
/** /**
* Search emails handler * Search emails handler
@ -233,7 +234,7 @@ function formatSearchResults(response) {
// Format results // Format results
const emailList = response.value.map((email, index) => { const emailList = response.value.map((email, index) => {
const sender = email.from?.emailAddress || { name: 'Unknown', address: 'unknown' }; const sender = email.from?.emailAddress || { name: 'Unknown', address: 'unknown' };
const date = new Date(email.receivedDateTime).toLocaleString(); const date = formatDateTime(email.receivedDateTime);
const readStatus = email.isRead ? '' : '[UNREAD] '; const readStatus = email.isRead ? '' : '[UNREAD] ';
const threadNote = email.conversationId ? `\nConversationID: ${email.conversationId}` : ''; const threadNote = email.conversationId ? `\nConversationID: ${email.conversationId}` : '';

View file

@ -1,13 +1,12 @@
/** /**
* Email rules management module for Outlook MCP server * Email rules management module for Outlook MCP server
*/ */
const handleListRules = require('./list');
const handleCreateRule = require('./create');
const { callGraphAPI } = require('../utils/graph-api'); const { callGraphAPI } = require('../utils/graph-api');
const { ensureAuthenticated } = require('../auth'); const { ensureAuthenticated } = require('../auth');
// Import getInboxRules for the edit sequence tool // Import rule handlers
const { getInboxRules } = require('./list'); const { handleListRules, getInboxRules } = require('./list');
const handleCreateRule = require('./create');
/** /**
* Edit rule sequence handler * Edit rule sequence handler

View file

@ -83,8 +83,14 @@ async function callGraphAPI(accessToken, method, path, data = null, queryParams
res.on('end', () => { res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) { if (res.statusCode >= 200 && res.statusCode < 300) {
const trimmed = responseData.trim();
if (!trimmed) {
// Graph returns empty bodies for some successful operations
// (e.g. sendMail 202, DELETE 204). Treat this as success.
return resolve({});
}
try { try {
const jsonResponse = JSON.parse(responseData); const jsonResponse = JSON.parse(trimmed);
resolve(jsonResponse); resolve(jsonResponse);
} catch (error) { } catch (error) {
reject(new Error(`Error parsing API response: ${error.message}`)); reject(new Error(`Error parsing API response: ${error.message}`));

View file

@ -2,6 +2,8 @@
const { cleanBody } = require('./bodyParser'); const { cleanBody } = require('./bodyParser');
const { formatDateTime } = require('../utils/time-formatter');
/** /**
* threadBuilder.js * threadBuilder.js
* Reconstructs a clean, deduplicated email thread from a set of Graph API * Reconstructs a clean, deduplicated email thread from a set of Graph API
@ -77,10 +79,7 @@ function formatThreadEntry(msg, index) {
.join(', '); .join(', ');
const date = msg.receivedDateTime const date = msg.receivedDateTime
? new Date(msg.receivedDateTime).toLocaleString('en-US', { ? formatDateTime(msg.receivedDateTime)
month: 'numeric', day: 'numeric', year: 'numeric',
hour: 'numeric', minute: '2-digit', hour12: true
})
: 'Unknown date'; : 'Unknown date';
const bodyText = msg.body?.content || msg.bodyPreview || ''; const bodyText = msg.body?.content || msg.bodyPreview || '';

42
utils/time-formatter.js Normal file
View file

@ -0,0 +1,42 @@
/**
* Timezone-aware date formatting utilities for Outlook MCP Server.
*
* All user-visible timestamps are formatted in the configured timezone
* (MS_TIMEZONE env var, default Eastern Standard Time) so that dates
* rendered inside the UTC container still match the user's local time.
*/
const config = require('../config');
/**
* Format an ISO 8601 or Graph dateTime string for display.
* @param {string} isoString - ISO 8601 / UTC timestamp from Graph API
* @param {string} [timeZone] - IANA or Windows timezone name (defaults to config.DEFAULT_TIMEZONE)
* @returns {string} Formatted date/time string with timezone abbreviation
*/
function formatDateTime(isoString, timeZone = config.DEFAULT_TIMEZONE) {
if (!isoString) return 'Unknown';
const d = new Date(isoString);
if (isNaN(d.getTime())) return String(isoString);
try {
return d.toLocaleString('en-US', {
timeZone,
month: 'numeric',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true,
timeZoneName: 'short'
});
} catch (err) {
// If the configured timezone identifier is invalid, fall back to ISO.
console.error(`Invalid timezone "${timeZone}", falling back to ISO: ${err.message}`);
return d.toISOString();
}
}
module.exports = {
formatDateTime
};