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:
parent
ba45f57b39
commit
68a64461eb
11 changed files with 71 additions and 18 deletions
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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`;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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` : '';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 = '';
|
||||||
|
|
|
||||||
|
|
@ -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 = '';
|
||||||
|
|
|
||||||
|
|
@ -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}` : '';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}`));
|
||||||
|
|
|
||||||
|
|
@ -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
42
utils/time-formatter.js
Normal 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
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue