@softeria/ms-365-mcp-server 0.150.0 → 0.150.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -414,7 +414,7 @@
414
414
  "toolName": "create-calendar-event",
415
415
  "presets": ["calendar", "outlook", "personal"],
416
416
  "scopes": ["Calendars.ReadWrite"],
417
- "descriptionOverride": "Create (schedule) a new calendar event — a meeting or appointment — on the user's calendar. Set subject, start/end times, time zone, location, body, and attendees; supports online meetings and recurrence.",
417
+ "descriptionOverride": "Create (schedule) a new calendar event — a meeting or appointment — on the user's calendar. Times use nested objects, not flat fields: start: {dateTime, timeZone}, end: {dateTime, timeZone}. Do NOT use startDateTime/startTimeZone. For one-off events, UTC is simplest (e.g. 3:30 PM AEDT = 04:30 UTC). For recurring events, use the organizer's own time zone name instead — Graph resolves DST against the zone in start.timeZone, so UTC drifts after DST changes. Get the zone from get-mailbox-settings, or validate one with list-supported-time-zones instead of guessing from memory. Set subject, location, body, and attendees; supports online meetings and recurrence.",
418
418
  "llmTip": "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."
419
419
  },
420
420
  {
@@ -423,6 +423,7 @@
423
423
  "toolName": "update-calendar-event",
424
424
  "presets": ["calendar", "outlook", "personal"],
425
425
  "scopes": ["Calendars.ReadWrite"],
426
+ "descriptionOverride": "Update an event on the default calendar. Requires eventId (the event's ID from get-calendar-view or list-calendar-events). Times use nested {dateTime, timeZone} objects. UTC is simplest for one-off events; for recurring events use the organizer's own time zone (from get-mailbox-settings or list-supported-time-zones) instead of UTC, since Graph resolves DST against that zone.",
426
427
  "llmTip": "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. WARNING: Setting attendees replaces the entire attendee list — include all attendees, not just new ones."
427
428
  },
428
429
  {
@@ -484,6 +485,7 @@
484
485
  "toolName": "create-specific-calendar-event",
485
486
  "presets": ["calendar", "outlook", "personal"],
486
487
  "scopes": ["Calendars.ReadWrite"],
488
+ "descriptionOverride": "Create a calendar event on a specific calendar. Requires calendarId (the target calendar's ID). Times use nested {dateTime, timeZone} objects — do NOT use startDateTime/startTimeZone. UTC is simplest for one-off events; for recurring events use the organizer's own time zone (from get-mailbox-settings or list-supported-time-zones) instead of UTC, since Graph resolves DST against that zone.",
487
489
  "llmTip": "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."
488
490
  },
489
491
  {
@@ -492,6 +494,7 @@
492
494
  "toolName": "update-specific-calendar-event",
493
495
  "presets": ["calendar", "outlook", "personal"],
494
496
  "scopes": ["Calendars.ReadWrite"],
497
+ "descriptionOverride": "Update a specific calendar event. Requires calendarId (from list-calendars) and eventId (from list-specific-calendar-events or get-specific-calendar-view for that same calendar). Times use nested {dateTime, timeZone} objects. UTC is simplest for one-off events; for recurring events use the organizer's own time zone (from get-mailbox-settings or list-supported-time-zones) instead of UTC, since Graph resolves DST against that zone.",
495
498
  "llmTip": "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. WARNING: Setting attendees replaces the entire attendee list — include all attendees, not just new ones."
496
499
  },
497
500
  {
@@ -500,6 +503,7 @@
500
503
  "toolName": "delete-specific-calendar-event",
501
504
  "presets": ["calendar", "outlook", "personal"],
502
505
  "scopes": ["Calendars.ReadWrite"],
506
+ "descriptionOverride": "Delete a specific calendar event. Requires calendarId (the target calendar's ID) and eventId (the event's own ID).",
503
507
  "llmTip": "Deleting a seriesMaster deletes ALL occurrences. To cancel a single occurrence, use the specific instance ID."
504
508
  },
505
509
  {
@@ -880,7 +884,7 @@
880
884
  "pathPattern": "/me/messages/{message-id}/$value",
881
885
  "method": "get",
882
886
  "toolName": "get-mail-message-mime",
883
- "descriptionOverride": "Download the raw MIME source (RFC 822 .eml content) of an Outlook email message by its message ID. Returns the complete original message including headers and encoded attachments.",
887
+ "descriptionOverride": "Download the raw MIME source (RFC 5322 .eml content) of an Outlook email message by its message ID. Returns the complete original message including headers and encoded attachments.",
884
888
  "presets": ["mail", "outlook", "personal"],
885
889
  "scopes": ["Mail.Read"],
886
890
  "acceptType": "text/plain",
@@ -1304,7 +1308,7 @@
1304
1308
  "toolName": "list-planner-tasks",
1305
1309
  "presets": ["tasks", "work"],
1306
1310
  "scopes": ["Tasks.Read"],
1307
- "llmTip": "Priority values: 0=Urgent, 1=Important, 3=Medium, 5=Low, 9=unset."
1311
+ "llmTip": "Priority is 0-10 (lower = higher priority); Planner's own UI presets are 1=Urgent, 3=Important, 5=Medium, 9=Low."
1308
1312
  },
1309
1313
  {
1310
1314
  "pathPattern": "/planner/plans/{plannerPlan-id}",
@@ -1319,7 +1323,7 @@
1319
1323
  "toolName": "list-plan-tasks",
1320
1324
  "presets": ["tasks", "work"],
1321
1325
  "scopes": ["Tasks.Read"],
1322
- "llmTip": "Priority values: 0=Urgent, 1=Important, 3=Medium, 5=Low, 9=unset."
1326
+ "llmTip": "Priority is 0-10 (lower = higher priority); Planner's own UI presets are 1=Urgent, 3=Important, 5=Medium, 9=Low."
1323
1327
  },
1324
1328
  {
1325
1329
  "pathPattern": "/planner/tasks/{plannerTask-id}",
@@ -1342,7 +1346,7 @@
1342
1346
  "toolName": "update-planner-task",
1343
1347
  "presets": ["tasks", "work"],
1344
1348
  "scopes": ["Tasks.ReadWrite"],
1345
- "llmTip": "CRITICAL: Requires If-Match header with the task's @odata.etag value, otherwise returns 412 Precondition Failed. Get the ETag from get-planner-task with includeHeaders=true. Priority values: 0=Urgent, 1=Important, 3=Medium, 5=Low, 9=unset."
1349
+ "llmTip": "CRITICAL: Requires If-Match header with the task's @odata.etag value, otherwise returns 412 Precondition Failed. Get the ETag from get-planner-task with includeHeaders=true. Priority is 0-10 (lower = higher priority); Planner's own UI presets are 1=Urgent, 3=Important, 5=Medium, 9=Low."
1346
1350
  },
1347
1351
  {
1348
1352
  "pathPattern": "/planner/tasks/{plannerTask-id}/details",
@@ -1529,7 +1533,7 @@
1529
1533
  "toolName": "list-relevant-people",
1530
1534
  "presets": ["users", "work"],
1531
1535
  "workScopes": ["People.Read"],
1532
- "llmTip": "Lists people most relevant to the current user, ordered by relevance. Based on communication patterns, collaboration, and business relationships. Each person has displayName, emailAddresses, jobTitle, department, officeLocation. Use $search to find specific people by name. Use $top to limit results."
1536
+ "llmTip": "Lists people most relevant to the current user, ordered by relevance. Based on communication patterns, collaboration, and business relationships. Each person has displayName, scoredEmailAddresses, jobTitle, department, officeLocation. Use $search to find specific people by name. Use $top to limit results."
1533
1537
  },
1534
1538
  {
1535
1539
  "pathPattern": "/me/memberOf",
@@ -2097,7 +2101,7 @@
2097
2101
  "toolName": "create-online-meeting",
2098
2102
  "presets": ["teams", "work"],
2099
2103
  "workScopes": ["OnlineMeetings.ReadWrite"],
2100
- "llmTip": "Creates a new online meeting. Required body: { subject, startDateTime, endDateTime }. Optional: participants (with organizer and attendees), lobbyBypassSettings, isEntryExitAnnounced, allowedPresenters (everyone/organization/roleIsPresenter/organizer). Returns the created meeting with joinWebUrl and meeting ID."
2104
+ "llmTip": "Creates a new online meeting. Required body: { subject, endDateTime }. startDateTime is not documented as required, but is commonly supplied. Optional: participants (with organizer and attendees), lobbyBypassSettings, isEntryExitAnnounced, allowedPresenters (everyone/organization/roleIsPresenter/organizer). Returns the created meeting with joinWebUrl and meeting ID."
2101
2105
  },
2102
2106
  {
2103
2107
  "pathPattern": "/me/onlineMeetings/{onlineMeeting-id}",
@@ -2548,7 +2552,7 @@
2548
2552
  "toolName": "list-supported-time-zones",
2549
2553
  "presets": ["calendar", "mail", "outlook", "personal"],
2550
2554
  "scopes": ["User.Read"],
2551
- "llmTip": "Lists time zones the user's mailbox server supports. TimeZoneStandard path parameter must be one of: Windows (default — Windows time zone names like 'Pacific Standard Time'), or Iana (IANA / Olson names like 'America/Los_Angeles'). Note the PascalCase — the values are case-sensitive enums, not lowercase strings. Returns timeZoneInformation objects with alias and displayName. Use the result to validate or look up the value before calling update-mailbox-settings to change the user's preferred timeZone — the format must match what the server expects."
2555
+ "llmTip": "Lists time zones the user's mailbox server supports. TimeZoneStandard path parameter must be one of: Windows (default — Windows time zone names like 'Pacific Standard Time'), or Iana (IANA / Olson names like 'America/Los_Angeles'). Note the PascalCase — the values are case-sensitive enums, not lowercase strings. Returns timeZoneInformation objects with alias and displayName. Use the result to validate or look up the value before calling update-mailbox-settings to change the user's preferred timeZone, or before setting timeZone on a calendar event's start/end (especially for recurring events) — don't guess a time zone name from memory, look it up here."
2552
2556
  },
2553
2557
  {
2554
2558
  "pathPattern": "/me/outlook/supportedLanguages()",
@@ -114,6 +114,61 @@ function toOAuthErrorResponse(error) {
114
114
  }
115
115
  };
116
116
  }
117
+ const PUBLIC_CLIENT_SECRET_REJECTED = 700025;
118
+ function isPublicClientRejection(body) {
119
+ return Array.isArray(body?.error_codes) && body.error_codes.includes(PUBLIC_CLIENT_SECRET_REJECTED);
120
+ }
121
+ async function postTokenRequest(tokenUrl, params) {
122
+ const response = await fetch(tokenUrl, {
123
+ method: "POST",
124
+ headers: {
125
+ "Content-Type": "application/x-www-form-urlencoded"
126
+ },
127
+ body: params
128
+ });
129
+ if (response.ok) {
130
+ return { ok: true, json: await response.json() };
131
+ }
132
+ const raw = await response.text();
133
+ return { ok: false, status: response.status, raw, parsed: parseUpstreamOAuthError(raw) };
134
+ }
135
+ async function requestToken(tokenUrl, params, clientSecret, failureMessage) {
136
+ if (!clientSecret) {
137
+ return unwrapToken(await postTokenRequest(tokenUrl, params), failureMessage);
138
+ }
139
+ const withSecret = new URLSearchParams(params);
140
+ withSecret.append("client_secret", clientSecret);
141
+ let result = await postTokenRequest(tokenUrl, withSecret);
142
+ if (!result.ok && isPublicClientRejection(result.parsed)) {
143
+ logger.info(
144
+ "Upstream rejected client_secret as a public client (AADSTS700025) \u2014 retrying without it",
145
+ {
146
+ status: result.status,
147
+ error_codes: result.parsed?.error_codes,
148
+ correlation_id: result.parsed?.correlation_id
149
+ }
150
+ );
151
+ result = await postTokenRequest(tokenUrl, params);
152
+ }
153
+ return unwrapToken(result, failureMessage);
154
+ }
155
+ function unwrapToken(result, failureMessage) {
156
+ if (result.ok) {
157
+ return result.json;
158
+ }
159
+ if (result.parsed) {
160
+ logger.warn(`Token endpoint upstream OAuth error: ${result.parsed.error}`, {
161
+ status: result.status,
162
+ error: result.parsed.error,
163
+ suberror: result.parsed.suberror,
164
+ error_codes: result.parsed.error_codes,
165
+ correlation_id: result.parsed.correlation_id
166
+ });
167
+ throw new OAuthUpstreamError(result.status, result.raw, result.parsed);
168
+ }
169
+ logger.error(`${failureMessage}: ${result.raw}`);
170
+ throw new Error(`${failureMessage}: ${result.raw}`);
171
+ }
117
172
  async function exchangeCodeForToken(code, redirectUri, clientId, clientSecret, tenantId = "common", codeVerifier, cloudType = "global") {
118
173
  const cloudEndpoints = getCloudEndpoints(cloudType);
119
174
  const params = new URLSearchParams({
@@ -122,36 +177,15 @@ async function exchangeCodeForToken(code, redirectUri, clientId, clientSecret, t
122
177
  redirect_uri: redirectUri,
123
178
  client_id: clientId
124
179
  });
125
- if (clientSecret) {
126
- params.append("client_secret", clientSecret);
127
- }
128
180
  if (codeVerifier) {
129
181
  params.append("code_verifier", codeVerifier);
130
182
  }
131
- const response = await fetch(`${cloudEndpoints.authority}/${tenantId}/oauth2/v2.0/token`, {
132
- method: "POST",
133
- headers: {
134
- "Content-Type": "application/x-www-form-urlencoded"
135
- },
136
- body: params
137
- });
138
- if (!response.ok) {
139
- const raw = await response.text();
140
- const parsed = parseUpstreamOAuthError(raw);
141
- if (parsed) {
142
- logger.warn(`Token endpoint upstream OAuth error: ${parsed.error}`, {
143
- status: response.status,
144
- error: parsed.error,
145
- suberror: parsed.suberror,
146
- error_codes: parsed.error_codes,
147
- correlation_id: parsed.correlation_id
148
- });
149
- throw new OAuthUpstreamError(response.status, raw, parsed);
150
- }
151
- logger.error(`Failed to exchange code for token: ${raw}`);
152
- throw new Error(`Failed to exchange code for token: ${raw}`);
153
- }
154
- return response.json();
183
+ return requestToken(
184
+ `${cloudEndpoints.authority}/${tenantId}/oauth2/v2.0/token`,
185
+ params,
186
+ clientSecret,
187
+ "Failed to exchange code for token"
188
+ );
155
189
  }
156
190
  async function refreshAccessToken(refreshToken, clientId, clientSecret, tenantId = "common", cloudType = "global") {
157
191
  const cloudEndpoints = getCloudEndpoints(cloudType);
@@ -160,33 +194,12 @@ async function refreshAccessToken(refreshToken, clientId, clientSecret, tenantId
160
194
  refresh_token: refreshToken,
161
195
  client_id: clientId
162
196
  });
163
- if (clientSecret) {
164
- params.append("client_secret", clientSecret);
165
- }
166
- const response = await fetch(`${cloudEndpoints.authority}/${tenantId}/oauth2/v2.0/token`, {
167
- method: "POST",
168
- headers: {
169
- "Content-Type": "application/x-www-form-urlencoded"
170
- },
171
- body: params
172
- });
173
- if (!response.ok) {
174
- const raw = await response.text();
175
- const parsed = parseUpstreamOAuthError(raw);
176
- if (parsed) {
177
- logger.warn(`Token endpoint upstream OAuth error: ${parsed.error}`, {
178
- status: response.status,
179
- error: parsed.error,
180
- suberror: parsed.suberror,
181
- error_codes: parsed.error_codes,
182
- correlation_id: parsed.correlation_id
183
- });
184
- throw new OAuthUpstreamError(response.status, raw, parsed);
185
- }
186
- logger.error(`Failed to refresh token: ${raw}`);
187
- throw new Error(`Failed to refresh token: ${raw}`);
188
- }
189
- return response.json();
197
+ return requestToken(
198
+ `${cloudEndpoints.authority}/${tenantId}/oauth2/v2.0/token`,
199
+ params,
200
+ clientSecret,
201
+ "Failed to refresh token"
202
+ );
190
203
  }
191
204
  export {
192
205
  OAuthUpstreamError,
package/dist/server.js CHANGED
@@ -45,6 +45,7 @@ function parseHttpOption(httpOption) {
45
45
  const port = parseInt(httpString) || 3e3;
46
46
  return { host: void 0, port };
47
47
  }
48
+ const PKCE_MAX_AGE_MS = 60 * 60 * 1e3;
48
49
  class MicrosoftGraphServer {
49
50
  constructor(authManager, options = {}) {
50
51
  this.version = "0.0.0";
@@ -318,17 +319,18 @@ class MicrosoftGraphServer {
318
319
  microsoftAuthUrl.searchParams.set(param, value);
319
320
  }
320
321
  });
321
- if (clientCodeChallenge && state) {
322
- const serverCodeVerifier = crypto.randomBytes(32).toString("base64url");
323
- const serverCodeChallenge = crypto.createHash("sha256").update(serverCodeVerifier).digest("base64url");
322
+ if (clientCodeChallenge) {
324
323
  const now = Date.now();
325
- const maxAge = 10 * 60 * 1e3;
326
- const maxEntries = 1e3;
327
324
  for (const [key, value] of this.pkceStore) {
328
- if (now - value.createdAt > maxAge) {
325
+ if (now - value.createdAt > PKCE_MAX_AGE_MS || value.clientCodeChallenge === clientCodeChallenge) {
329
326
  this.pkceStore.delete(key);
330
327
  }
331
328
  }
329
+ }
330
+ if (clientCodeChallenge && state) {
331
+ const serverCodeVerifier = crypto.randomBytes(32).toString("base64url");
332
+ const serverCodeChallenge = crypto.createHash("sha256").update(serverCodeVerifier).digest("base64url");
333
+ const maxEntries = 1e3;
332
334
  if (this.pkceStore.size >= maxEntries) {
333
335
  logger.warn(
334
336
  `PKCE store at capacity (${maxEntries} entries) \u2014 rejecting new authorization request`
@@ -341,7 +343,6 @@ class MicrosoftGraphServer {
341
343
  }
342
344
  this.pkceStore.set(state, {
343
345
  clientCodeChallenge,
344
- clientCodeChallengeMethod: clientCodeChallengeMethod || "S256",
345
346
  serverCodeVerifier,
346
347
  createdAt: Date.now()
347
348
  });
@@ -405,14 +406,15 @@ class MicrosoftGraphServer {
405
406
  tenantId,
406
407
  hasClientSecret: !!clientSecret
407
408
  });
408
- let serverCodeVerifier;
409
+ let matchedPkceState;
410
+ let matchedPkceEntry;
409
411
  if (body.code_verifier) {
410
412
  const clientVerifier = body.code_verifier;
411
413
  const clientChallengeComputed = crypto.createHash("sha256").update(clientVerifier).digest("base64url");
412
414
  for (const [state, pkceData] of this.pkceStore) {
413
415
  if (pkceData.clientCodeChallenge === clientChallengeComputed) {
414
- serverCodeVerifier = pkceData.serverCodeVerifier;
415
- this.pkceStore.delete(state);
416
+ matchedPkceState = state;
417
+ matchedPkceEntry = pkceData;
416
418
  logger.info("Two-leg PKCE: matched client verifier, using server verifier", {
417
419
  state: state.substring(0, 8) + "..."
418
420
  });
@@ -426,9 +428,12 @@ class MicrosoftGraphServer {
426
428
  clientId,
427
429
  clientSecret,
428
430
  tenantId,
429
- serverCodeVerifier || body.code_verifier,
431
+ matchedPkceEntry?.serverCodeVerifier || body.code_verifier,
430
432
  this.secrets.cloudType
431
433
  );
434
+ if (matchedPkceState && this.pkceStore.get(matchedPkceState) === matchedPkceEntry) {
435
+ this.pkceStore.delete(matchedPkceState);
436
+ }
432
437
  res.json(result);
433
438
  } else if (body.grant_type === "refresh_token") {
434
439
  const tenantId = this.secrets?.tenantId || "common";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@softeria/ms-365-mcp-server",
3
3
  "mcpName": "io.github.Softeria/ms-365-mcp-server",
4
- "version": "0.150.0",
4
+ "version": "0.150.2",
5
5
  "description": " A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Office services through the Graph API",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -37,7 +37,7 @@
37
37
  "dependencies": {
38
38
  "@azure/msal-node": "^5.2.2",
39
39
  "@modelcontextprotocol/sdk": "^1.29.0",
40
- "@toon-format/toon": "^0.8.0",
40
+ "@toon-format/toon": "^2.3.1",
41
41
  "commander": "^11.1.0",
42
42
  "dotenv": "^17.0.1",
43
43
  "express": "^5.2.1",
@@ -414,7 +414,7 @@
414
414
  "toolName": "create-calendar-event",
415
415
  "presets": ["calendar", "outlook", "personal"],
416
416
  "scopes": ["Calendars.ReadWrite"],
417
- "descriptionOverride": "Create (schedule) a new calendar event — a meeting or appointment — on the user's calendar. Set subject, start/end times, time zone, location, body, and attendees; supports online meetings and recurrence.",
417
+ "descriptionOverride": "Create (schedule) a new calendar event — a meeting or appointment — on the user's calendar. Times use nested objects, not flat fields: start: {dateTime, timeZone}, end: {dateTime, timeZone}. Do NOT use startDateTime/startTimeZone. For one-off events, UTC is simplest (e.g. 3:30 PM AEDT = 04:30 UTC). For recurring events, use the organizer's own time zone name instead — Graph resolves DST against the zone in start.timeZone, so UTC drifts after DST changes. Get the zone from get-mailbox-settings, or validate one with list-supported-time-zones instead of guessing from memory. Set subject, location, body, and attendees; supports online meetings and recurrence.",
418
418
  "llmTip": "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."
419
419
  },
420
420
  {
@@ -423,6 +423,7 @@
423
423
  "toolName": "update-calendar-event",
424
424
  "presets": ["calendar", "outlook", "personal"],
425
425
  "scopes": ["Calendars.ReadWrite"],
426
+ "descriptionOverride": "Update an event on the default calendar. Requires eventId (the event's ID from get-calendar-view or list-calendar-events). Times use nested {dateTime, timeZone} objects. UTC is simplest for one-off events; for recurring events use the organizer's own time zone (from get-mailbox-settings or list-supported-time-zones) instead of UTC, since Graph resolves DST against that zone.",
426
427
  "llmTip": "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. WARNING: Setting attendees replaces the entire attendee list — include all attendees, not just new ones."
427
428
  },
428
429
  {
@@ -484,6 +485,7 @@
484
485
  "toolName": "create-specific-calendar-event",
485
486
  "presets": ["calendar", "outlook", "personal"],
486
487
  "scopes": ["Calendars.ReadWrite"],
488
+ "descriptionOverride": "Create a calendar event on a specific calendar. Requires calendarId (the target calendar's ID). Times use nested {dateTime, timeZone} objects — do NOT use startDateTime/startTimeZone. UTC is simplest for one-off events; for recurring events use the organizer's own time zone (from get-mailbox-settings or list-supported-time-zones) instead of UTC, since Graph resolves DST against that zone.",
487
489
  "llmTip": "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."
488
490
  },
489
491
  {
@@ -492,6 +494,7 @@
492
494
  "toolName": "update-specific-calendar-event",
493
495
  "presets": ["calendar", "outlook", "personal"],
494
496
  "scopes": ["Calendars.ReadWrite"],
497
+ "descriptionOverride": "Update a specific calendar event. Requires calendarId (from list-calendars) and eventId (from list-specific-calendar-events or get-specific-calendar-view for that same calendar). Times use nested {dateTime, timeZone} objects. UTC is simplest for one-off events; for recurring events use the organizer's own time zone (from get-mailbox-settings or list-supported-time-zones) instead of UTC, since Graph resolves DST against that zone.",
495
498
  "llmTip": "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. WARNING: Setting attendees replaces the entire attendee list — include all attendees, not just new ones."
496
499
  },
497
500
  {
@@ -500,6 +503,7 @@
500
503
  "toolName": "delete-specific-calendar-event",
501
504
  "presets": ["calendar", "outlook", "personal"],
502
505
  "scopes": ["Calendars.ReadWrite"],
506
+ "descriptionOverride": "Delete a specific calendar event. Requires calendarId (the target calendar's ID) and eventId (the event's own ID).",
503
507
  "llmTip": "Deleting a seriesMaster deletes ALL occurrences. To cancel a single occurrence, use the specific instance ID."
504
508
  },
505
509
  {
@@ -880,7 +884,7 @@
880
884
  "pathPattern": "/me/messages/{message-id}/$value",
881
885
  "method": "get",
882
886
  "toolName": "get-mail-message-mime",
883
- "descriptionOverride": "Download the raw MIME source (RFC 822 .eml content) of an Outlook email message by its message ID. Returns the complete original message including headers and encoded attachments.",
887
+ "descriptionOverride": "Download the raw MIME source (RFC 5322 .eml content) of an Outlook email message by its message ID. Returns the complete original message including headers and encoded attachments.",
884
888
  "presets": ["mail", "outlook", "personal"],
885
889
  "scopes": ["Mail.Read"],
886
890
  "acceptType": "text/plain",
@@ -1304,7 +1308,7 @@
1304
1308
  "toolName": "list-planner-tasks",
1305
1309
  "presets": ["tasks", "work"],
1306
1310
  "scopes": ["Tasks.Read"],
1307
- "llmTip": "Priority values: 0=Urgent, 1=Important, 3=Medium, 5=Low, 9=unset."
1311
+ "llmTip": "Priority is 0-10 (lower = higher priority); Planner's own UI presets are 1=Urgent, 3=Important, 5=Medium, 9=Low."
1308
1312
  },
1309
1313
  {
1310
1314
  "pathPattern": "/planner/plans/{plannerPlan-id}",
@@ -1319,7 +1323,7 @@
1319
1323
  "toolName": "list-plan-tasks",
1320
1324
  "presets": ["tasks", "work"],
1321
1325
  "scopes": ["Tasks.Read"],
1322
- "llmTip": "Priority values: 0=Urgent, 1=Important, 3=Medium, 5=Low, 9=unset."
1326
+ "llmTip": "Priority is 0-10 (lower = higher priority); Planner's own UI presets are 1=Urgent, 3=Important, 5=Medium, 9=Low."
1323
1327
  },
1324
1328
  {
1325
1329
  "pathPattern": "/planner/tasks/{plannerTask-id}",
@@ -1342,7 +1346,7 @@
1342
1346
  "toolName": "update-planner-task",
1343
1347
  "presets": ["tasks", "work"],
1344
1348
  "scopes": ["Tasks.ReadWrite"],
1345
- "llmTip": "CRITICAL: Requires If-Match header with the task's @odata.etag value, otherwise returns 412 Precondition Failed. Get the ETag from get-planner-task with includeHeaders=true. Priority values: 0=Urgent, 1=Important, 3=Medium, 5=Low, 9=unset."
1349
+ "llmTip": "CRITICAL: Requires If-Match header with the task's @odata.etag value, otherwise returns 412 Precondition Failed. Get the ETag from get-planner-task with includeHeaders=true. Priority is 0-10 (lower = higher priority); Planner's own UI presets are 1=Urgent, 3=Important, 5=Medium, 9=Low."
1346
1350
  },
1347
1351
  {
1348
1352
  "pathPattern": "/planner/tasks/{plannerTask-id}/details",
@@ -1529,7 +1533,7 @@
1529
1533
  "toolName": "list-relevant-people",
1530
1534
  "presets": ["users", "work"],
1531
1535
  "workScopes": ["People.Read"],
1532
- "llmTip": "Lists people most relevant to the current user, ordered by relevance. Based on communication patterns, collaboration, and business relationships. Each person has displayName, emailAddresses, jobTitle, department, officeLocation. Use $search to find specific people by name. Use $top to limit results."
1536
+ "llmTip": "Lists people most relevant to the current user, ordered by relevance. Based on communication patterns, collaboration, and business relationships. Each person has displayName, scoredEmailAddresses, jobTitle, department, officeLocation. Use $search to find specific people by name. Use $top to limit results."
1533
1537
  },
1534
1538
  {
1535
1539
  "pathPattern": "/me/memberOf",
@@ -2097,7 +2101,7 @@
2097
2101
  "toolName": "create-online-meeting",
2098
2102
  "presets": ["teams", "work"],
2099
2103
  "workScopes": ["OnlineMeetings.ReadWrite"],
2100
- "llmTip": "Creates a new online meeting. Required body: { subject, startDateTime, endDateTime }. Optional: participants (with organizer and attendees), lobbyBypassSettings, isEntryExitAnnounced, allowedPresenters (everyone/organization/roleIsPresenter/organizer). Returns the created meeting with joinWebUrl and meeting ID."
2104
+ "llmTip": "Creates a new online meeting. Required body: { subject, endDateTime }. startDateTime is not documented as required, but is commonly supplied. Optional: participants (with organizer and attendees), lobbyBypassSettings, isEntryExitAnnounced, allowedPresenters (everyone/organization/roleIsPresenter/organizer). Returns the created meeting with joinWebUrl and meeting ID."
2101
2105
  },
2102
2106
  {
2103
2107
  "pathPattern": "/me/onlineMeetings/{onlineMeeting-id}",
@@ -2548,7 +2552,7 @@
2548
2552
  "toolName": "list-supported-time-zones",
2549
2553
  "presets": ["calendar", "mail", "outlook", "personal"],
2550
2554
  "scopes": ["User.Read"],
2551
- "llmTip": "Lists time zones the user's mailbox server supports. TimeZoneStandard path parameter must be one of: Windows (default — Windows time zone names like 'Pacific Standard Time'), or Iana (IANA / Olson names like 'America/Los_Angeles'). Note the PascalCase — the values are case-sensitive enums, not lowercase strings. Returns timeZoneInformation objects with alias and displayName. Use the result to validate or look up the value before calling update-mailbox-settings to change the user's preferred timeZone — the format must match what the server expects."
2555
+ "llmTip": "Lists time zones the user's mailbox server supports. TimeZoneStandard path parameter must be one of: Windows (default — Windows time zone names like 'Pacific Standard Time'), or Iana (IANA / Olson names like 'America/Los_Angeles'). Note the PascalCase — the values are case-sensitive enums, not lowercase strings. Returns timeZoneInformation objects with alias and displayName. Use the result to validate or look up the value before calling update-mailbox-settings to change the user's preferred timeZone, or before setting timeZone on a calendar event's start/end (especially for recurring events) — don't guess a time zone name from memory, look it up here."
2552
2556
  },
2553
2557
  {
2554
2558
  "pathPattern": "/me/outlook/supportedLanguages()",