@chrischall/office-outlook-mcp 0.4.1 → 0.5.0

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.
package/dist/bundle.js CHANGED
@@ -35616,6 +35616,11 @@ function textResult(data) {
35616
35616
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
35617
35617
  };
35618
35618
  }
35619
+ function imageResult(base643, mimeType) {
35620
+ return {
35621
+ content: [{ type: "image", data: base643, mimeType }]
35622
+ };
35623
+ }
35619
35624
  function errorResult(message) {
35620
35625
  return {
35621
35626
  content: [{ type: "text", text: redactSecrets(message) }],
@@ -37474,6 +37479,18 @@ var init_verify_after_write = __esm({
37474
37479
  });
37475
37480
 
37476
37481
  // node_modules/@chrischall/mcp-utils/dist/concurrency/index.js
37482
+ async function mapWithConcurrency(items, limit, fn) {
37483
+ const results = new Array(items.length);
37484
+ let next = 0;
37485
+ const poolSize = Math.min(Math.max(limit, 1), items.length);
37486
+ await Promise.all(Array.from({ length: poolSize }, async () => {
37487
+ while (next < items.length) {
37488
+ const i = next++;
37489
+ results[i] = await fn(items[i], i);
37490
+ }
37491
+ }));
37492
+ return results;
37493
+ }
37477
37494
  var init_concurrency = __esm({
37478
37495
  "node_modules/@chrischall/mcp-utils/dist/concurrency/index.js"() {
37479
37496
  init_single_flight();
@@ -37571,7 +37588,7 @@ var init_version = __esm({
37571
37588
  "src/version.ts"() {
37572
37589
  "use strict";
37573
37590
  PACKAGE_NAME = "office-outlook-mcp";
37574
- VERSION = "0.4.1";
37591
+ VERSION = "0.5.0";
37575
37592
  }
37576
37593
  });
37577
37594
 
@@ -48663,6 +48680,10 @@ function toBearerTokens(accessToken) {
48663
48680
  }
48664
48681
  return { accessToken, refreshToken: RECAPTURE_SENTINEL, expiresAt };
48665
48682
  }
48683
+ function isCredentialFailure(e) {
48684
+ const kind = errorKindOf(e);
48685
+ return kind === "no_credential" || kind === "credential_rejected" || errorStatusOf(e) === 401;
48686
+ }
48666
48687
  var OutlookClient = class {
48667
48688
  #baseUrl;
48668
48689
  /**
@@ -48690,7 +48711,8 @@ var OutlookClient = class {
48690
48711
  this.#configError = new McpToolError(
48691
48712
  "No Outlook credential is configured.",
48692
48713
  {
48693
- hint: "Set OUTLOOK_ACCESS_TOKEN, or unset OUTLOOK_DISABLE_FETCHPROXY so the token can be captured from a signed-in Outlook browser tab."
48714
+ hint: "Set OUTLOOK_ACCESS_TOKEN, or unset OUTLOOK_DISABLE_FETCHPROXY so the token can be captured from a signed-in Outlook browser tab.",
48715
+ kind: "no_credential"
48694
48716
  }
48695
48717
  );
48696
48718
  }
@@ -48721,7 +48743,9 @@ var OutlookClient = class {
48721
48743
  refresh: async () => {
48722
48744
  if (direct !== void 0) {
48723
48745
  throw new McpToolError("OUTLOOK_ACCESS_TOKEN was rejected by Outlook.", {
48724
- hint: "A directly-supplied token cannot be refreshed automatically. Replace OUTLOOK_ACCESS_TOKEN with a current one, or unset it to capture from a signed-in Outlook browser tab instead."
48746
+ hint: "A directly-supplied token cannot be refreshed automatically. Replace OUTLOOK_ACCESS_TOKEN with a current one, or unset it to capture from a signed-in Outlook browser tab instead.",
48747
+ status: 401,
48748
+ kind: "credential_rejected"
48725
48749
  });
48726
48750
  }
48727
48751
  return toBearerTokens(await this.#capture());
@@ -48760,7 +48784,9 @@ var OutlookClient = class {
48760
48784
  timeout: 6e4,
48761
48785
  baseHeaders: { Accept: "application/json" },
48762
48786
  onUnauthorized: () => new McpToolError("Outlook rejected the access token (401).", {
48763
- hint: "The token has expired or been revoked. Open a signed-in Outlook tab and retry \u2014 the next call re-captures automatically."
48787
+ hint: "The token has expired or been revoked. Open a signed-in Outlook tab and retry \u2014 the next call re-captures automatically.",
48788
+ status: 401,
48789
+ kind: "credential_rejected"
48764
48790
  })
48765
48791
  });
48766
48792
  return this.#api;
@@ -48883,12 +48909,21 @@ function addrs(list) {
48883
48909
  if (!Array.isArray(list) || list.length === 0) return void 0;
48884
48910
  return list.map((r) => addr(r)).filter((s) => s !== void 0);
48885
48911
  }
48912
+ var MAX_RECIPIENTS = 20;
48913
+ function cappedRecipients(key, list) {
48914
+ if (list === void 0 || list.length <= MAX_RECIPIENTS) return { [key]: list };
48915
+ return {
48916
+ [key]: list.slice(0, MAX_RECIPIENTS),
48917
+ [`${key}Count`]: list.length,
48918
+ [`${key}Truncated`]: true
48919
+ };
48920
+ }
48886
48921
  function compactMessage(m) {
48887
48922
  return pruned({
48888
48923
  Id: m.Id,
48889
48924
  Subject: m.Subject,
48890
48925
  From: addr(m.From ?? m.Sender),
48891
- To: addrs(m.ToRecipients),
48926
+ ...cappedRecipients("To", addrs(m.ToRecipients)),
48892
48927
  Received: m.ReceivedDateTime,
48893
48928
  IsRead: m.IsRead,
48894
48929
  HasAttachments: m.HasAttachments || void 0,
@@ -48901,7 +48936,7 @@ function compactMessage(m) {
48901
48936
  function fullMessage(m) {
48902
48937
  return pruned({
48903
48938
  ...compactMessage(m),
48904
- Cc: addrs(m.CcRecipients),
48939
+ ...cappedRecipients("Cc", addrs(m.CcRecipients)),
48905
48940
  Sent: m.SentDateTime,
48906
48941
  Importance: m.Importance,
48907
48942
  IsDraft: m.IsDraft,
@@ -48920,14 +48955,19 @@ function compactEvent(e) {
48920
48955
  TimeZone: e.Start?.TimeZone,
48921
48956
  Location: e.Location?.DisplayName || void 0,
48922
48957
  Organizer: addr(e.Organizer),
48923
- IsAllDay: e.IsAllDay || void 0,
48924
- IsCancelled: e.IsCancelled || void 0
48958
+ IsAllDay: e.IsAllDay,
48959
+ IsCancelled: e.IsCancelled,
48960
+ IsOrganizer: e.IsOrganizer,
48961
+ MyResponse: e.ResponseStatus?.Response,
48962
+ ResponseRequested: e.ResponseRequested,
48963
+ Type: e.Type,
48964
+ ShowAs: e.ShowAs
48925
48965
  });
48926
48966
  }
48927
48967
  function fullEvent(e) {
48928
48968
  return pruned({
48929
48969
  ...compactEvent(e),
48930
- ShowAs: e.ShowAs,
48970
+ SeriesMasterId: e.SeriesMasterId,
48931
48971
  Attendees: e.Attendees?.map(
48932
48972
  (a) => pruned({ Who: addr(a), Response: a.Status?.Response })
48933
48973
  ),
@@ -48975,6 +49015,17 @@ function projectCollection(payload, project, label) {
48975
49015
  });
48976
49016
  }
48977
49017
 
49018
+ // src/tools/_invite.ts
49019
+ var EVENT_EXPAND = "Microsoft.OutlookServices.EventMessage/Event";
49020
+ function resolvedEvent(e) {
49021
+ return e?.Id ? e : void 0;
49022
+ }
49023
+ var INVITE_GONE = "the meeting is no longer on your calendar (already declined, cancelled, or deleted), so there is nothing to respond to";
49024
+ var MEETING_MESSAGE_TYPE = "Microsoft.OutlookServices.EventMessage/MeetingMessageType";
49025
+ function isMeetingMessage(m) {
49026
+ return !!m?.MeetingMessageType || (m?.["@odata.type"] ?? "").includes("EventMessage");
49027
+ }
49028
+
48978
49029
  // src/tools/_paging.ts
48979
49030
  init_zod();
48980
49031
  var nextLinkParam = external_exports.string().min(1).optional().describe(
@@ -49016,6 +49067,91 @@ var LIST_SELECT = "Id,Subject,From,ToRecipients,ReceivedDateTime,IsRead,HasAttac
49016
49067
  function folderSegment(folder) {
49017
49068
  return encodeURIComponent(folder);
49018
49069
  }
49070
+ var UNREAD_SELECT = [
49071
+ "Id",
49072
+ "Subject",
49073
+ "From",
49074
+ "ToRecipients",
49075
+ "CcRecipients",
49076
+ "ReceivedDateTime",
49077
+ "Importance",
49078
+ "HasAttachments",
49079
+ "Categories",
49080
+ "Flag",
49081
+ "ConversationId",
49082
+ MEETING_MESSAGE_TYPE
49083
+ ].join(",");
49084
+ var BODY_CONCURRENCY = 4;
49085
+ function triageKind(m) {
49086
+ switch (m.MeetingMessageType) {
49087
+ case "MeetingRequest":
49088
+ return "meetingRequest";
49089
+ case "MeetingCancelled":
49090
+ return "meetingCancelled";
49091
+ case "MeetingAccepted":
49092
+ case "MeetingTenativelyAccepted":
49093
+ // sic — Outlook's spelling
49094
+ case "MeetingTentativelyAccepted":
49095
+ case "MeetingDeclined":
49096
+ return "meetingResponse";
49097
+ }
49098
+ const type = m["@odata.type"] ?? "";
49099
+ if (!type.includes("EventMessage")) return "mail";
49100
+ if (/Request$/.test(type)) return "meetingRequest";
49101
+ if (/Cancel/.test(type)) return "meetingCancelled";
49102
+ return "meetingResponse";
49103
+ }
49104
+ function triageEvent(e) {
49105
+ return stripUndefined({
49106
+ id: e.Id,
49107
+ subject: e.Subject,
49108
+ start: e.Start?.DateTime,
49109
+ end: e.End?.DateTime,
49110
+ timeZone: e.Start?.TimeZone,
49111
+ location: e.Location?.DisplayName || void 0,
49112
+ organizer: addr(e.Organizer),
49113
+ responseStatus: e.ResponseStatus?.Response,
49114
+ isCancelled: e.IsCancelled
49115
+ });
49116
+ }
49117
+ var INVITE_UNRESOLVED_HINT = `This invite's ${INVITE_GONE}.`;
49118
+ function truncateChars(text, max) {
49119
+ const end = /[\uD800-\uDBFF]/.test(text.charAt(max - 1)) ? max - 1 : max;
49120
+ return text.slice(0, end);
49121
+ }
49122
+ function stripUndefined(o) {
49123
+ return Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
49124
+ }
49125
+ var ATTACHMENT_META_SELECT = "Id,Name,ContentType,Size,IsInline";
49126
+ var MAX_INLINE_BYTES = 5 * 1024 * 1024;
49127
+ var MAX_TEXT_BYTES = 200 * 1024;
49128
+ var TEXT_TYPE = /^text\/|^application\/(json|xml|csv|ics|calendar)\b|\+(json|xml)$/i;
49129
+ var TEXT_EXT = /\.(txt|csv|tsv|json|xml|ics|md|log|html?)$/i;
49130
+ function attachmentKind(a) {
49131
+ const type = a["@odata.type"] ?? "";
49132
+ if (/ItemAttachment$/.test(type)) return "item";
49133
+ if (/ReferenceAttachment$/.test(type)) return "reference";
49134
+ return "file";
49135
+ }
49136
+ function isImage(contentType) {
49137
+ return /^image\//i.test(contentType) && !/svg/i.test(contentType);
49138
+ }
49139
+ function isText(contentType, name) {
49140
+ return TEXT_TYPE.test(contentType) || /svg/i.test(contentType) || TEXT_EXT.test(name);
49141
+ }
49142
+ function base64DecodedLength(b64) {
49143
+ const padding = b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0;
49144
+ return Math.floor(b64.length * 3 / 4) - padding;
49145
+ }
49146
+ function utf8Boundary(buf, max) {
49147
+ if (buf.length <= max) return buf.length;
49148
+ let end = max;
49149
+ while (end > 0 && (buf[end] & 192) === 128) end--;
49150
+ return end;
49151
+ }
49152
+ function sinceLiteral(hours) {
49153
+ return new Date(Date.now() - hours * 36e5).toISOString().replace(/\.\d{3}Z$/, "Z");
49154
+ }
49019
49155
  function registerMailTools(server, client2) {
49020
49156
  server.registerTool(
49021
49157
  "outlook_list_folders",
@@ -49044,7 +49180,7 @@ function registerMailTools(server, client2) {
49044
49180
  server.registerTool(
49045
49181
  "outlook_list_messages",
49046
49182
  {
49047
- description: "List messages in a folder, newest first. Defaults to the inbox. Use `unreadOnly` for a triage view. Bodies are NOT included \u2014 call outlook_get_message for one. Note `search` and `unreadOnly` cannot be combined on a first-page request (the API rejects $search with $filter); pass `search` alone to search the whole mailbox. When following a `nextLink`, both are ignored, so echoing them back is harmless. " + UNTRUSTED_DESCRIPTION_SUFFIX,
49183
+ description: 'List messages in a folder, newest first. Defaults to the inbox. Use `unreadOnly` for a triage view. Bodies are NOT included \u2014 call outlook_get_message for one. Note `search` and `unreadOnly` cannot be combined on a first-page request (the API rejects $search with $filter); pass `search` alone to search the whole mailbox. When following a `nextLink`, both are ignored, so echoing them back is harmless. Each message\'s To list (and Cc, in the full view) holds at most 20 addresses; a longer list adds ToCount/ToTruncated (CcCount/CcTruncated) with the full count \u2014 pass view:"raw" for every address. ' + UNTRUSTED_DESCRIPTION_SUFFIX,
49048
49184
  annotations: { readOnlyHint: true, openWorldHint: true },
49049
49185
  inputSchema: external_exports.object({
49050
49186
  view: viewParam(VIEWS2),
@@ -49090,7 +49226,7 @@ function registerMailTools(server, client2) {
49090
49226
  server.registerTool(
49091
49227
  "outlook_get_message",
49092
49228
  {
49093
- description: `Get one message including its body. The body is requested as plain text rather than HTML (measured ~9x smaller and far easier to read); pass view:"raw" to get Outlook's untouched record. ` + UNTRUSTED_DESCRIPTION_SUFFIX,
49229
+ description: `Get one message including its body. The body is requested as plain text rather than HTML (measured ~9x smaller and far easier to read); pass view:"raw" to get Outlook's untouched record. To and Cc list at most 20 addresses; a longer list adds ToCount/ToTruncated (CcCount/CcTruncated) with the full count \u2014 view:"raw" returns every address. ` + UNTRUSTED_DESCRIPTION_SUFFIX,
49094
49230
  annotations: { readOnlyHint: true, openWorldHint: true },
49095
49231
  inputSchema: external_exports.object({
49096
49232
  view: viewParam(VIEWS2),
@@ -49107,6 +49243,91 @@ function registerMailTools(server, client2) {
49107
49243
  return mailboxUntrusted(v === "full" ? fullMessage(data) : compactMessage(data));
49108
49244
  }
49109
49245
  );
49246
+ server.registerTool(
49247
+ "outlook_get_unread",
49248
+ {
49249
+ description: 'Triage batch: the unread messages in a folder (default inbox), newest first, each WITH its plain-text body (truncated to `maxBodyChars`) in one call \u2014 no follow-up outlook_get_message needed. Every item has a `kind`: "mail", "meetingRequest", "meetingCancelled" or "meetingResponse" (a reply to an invite you sent). Requests and cancellations also carry `event` (id, time, organizer, your current `responseStatus`) so the invite can be answered by its event id (when the meeting is no longer on your calendar \u2014 already declined, cancelled or deleted \u2014 the item has a `hint` instead and there is nothing to respond to). `to`/`cc` list at most 20 addresses; a longer list adds `toCount`/`ccCount` and `toTruncated`/`ccTruncated`. This does NOT mark anything read. Processing loop: read the batch \u2192 act on each item (reply, respond to the invite, file, flag \u2014 or leave it) \u2192 call outlook_mark_read on the ids you handled, so the next call returns only what is still new. Follow `nextLink` for more. ' + UNTRUSTED_DESCRIPTION_SUFFIX,
49250
+ annotations: { readOnlyHint: true, openWorldHint: true },
49251
+ inputSchema: external_exports.object({
49252
+ folder: external_exports.string().min(1).optional().describe('Folder id or well-known name (default "inbox")'),
49253
+ limit: external_exports.number().int().min(1).max(50).optional().describe("Max messages (default 25, max 50)"),
49254
+ sinceHours: external_exports.number().positive().max(24 * 365).optional().describe("Only mail received in the last N hours"),
49255
+ includeBody: external_exports.boolean().optional().describe("Fetch each plain-text body (default true). Invite events are resolved either way."),
49256
+ maxBodyChars: external_exports.number().int().min(100).max(1e5).optional().describe("Truncate each body to this many characters (default 4000); `bodyTruncated` marks a cut"),
49257
+ nextLink: nextLinkParam
49258
+ })
49259
+ },
49260
+ async ({ folder, limit, sinceHours, includeBody, maxBodyChars, nextLink }) => {
49261
+ const filter = [
49262
+ sinceHours !== void 0 ? `ReceivedDateTime ge ${sinceLiteral(sinceHours)}` : void 0,
49263
+ "IsRead eq false"
49264
+ ].filter((c) => c !== void 0).join(" and ");
49265
+ const data = await fetchPage(
49266
+ client2,
49267
+ nextLink,
49268
+ `/me/mailfolders/${folderSegment(folder ?? "inbox")}/messages`,
49269
+ {
49270
+ query: {
49271
+ $top: limit ?? 25,
49272
+ $select: UNREAD_SELECT,
49273
+ $filter: filter,
49274
+ $orderby: "ReceivedDateTime desc"
49275
+ }
49276
+ }
49277
+ );
49278
+ const rows = Array.isArray(data?.value) ? data.value : [];
49279
+ const wantBody = includeBody !== false;
49280
+ const cap = maxBodyChars ?? 4e3;
49281
+ const items = await mapWithConcurrency(rows, BODY_CONCURRENCY, async (m) => {
49282
+ const kind = triageKind(m);
49283
+ const item = stripUndefined({
49284
+ id: m.Id,
49285
+ kind,
49286
+ receivedAt: m.ReceivedDateTime,
49287
+ from: addr(m.From ?? m.Sender),
49288
+ ...cappedRecipients("to", addrs(m.ToRecipients)),
49289
+ ...cappedRecipients("cc", addrs(m.CcRecipients)),
49290
+ subject: m.Subject,
49291
+ importance: m.Importance,
49292
+ hasAttachments: m.HasAttachments || void 0,
49293
+ categories: m.Categories?.length ? m.Categories : void 0,
49294
+ flag: m.Flag?.FlagStatus,
49295
+ conversationId: m.ConversationId
49296
+ });
49297
+ const wantEvent = kind === "meetingRequest" || kind === "meetingCancelled";
49298
+ if (!wantBody && !wantEvent || m.Id === void 0) return item;
49299
+ try {
49300
+ const one = await client2.get(
49301
+ `/me/messages/${encodeURIComponent(m.Id)}`,
49302
+ {
49303
+ text: wantBody,
49304
+ query: {
49305
+ $select: wantBody ? "Id,Body" : "Id",
49306
+ $expand: wantEvent ? EVENT_EXPAND : void 0
49307
+ }
49308
+ }
49309
+ );
49310
+ if (wantBody) {
49311
+ const text = (one.Body?.Content ?? "").trim();
49312
+ item.body = text.length > cap ? truncateChars(text, cap) : text;
49313
+ if (text.length > cap) item.bodyTruncated = true;
49314
+ }
49315
+ if (wantEvent) {
49316
+ const event = resolvedEvent(one.Event);
49317
+ if (event) item.event = triageEvent(event);
49318
+ else item.hint = INVITE_UNRESOLVED_HINT;
49319
+ }
49320
+ } catch (e) {
49321
+ item.error = e instanceof Error ? e.message : String(e);
49322
+ }
49323
+ return item;
49324
+ });
49325
+ const out = { count: items.length, items };
49326
+ const next = data?.["@odata.nextLink"];
49327
+ if (next) out.nextLink = next;
49328
+ return mailboxUntrusted(out);
49329
+ }
49330
+ );
49110
49331
  server.registerTool(
49111
49332
  "outlook_list_attachments",
49112
49333
  {
@@ -49127,6 +49348,70 @@ function registerMailTools(server, client2) {
49127
49348
  return minifiedResult(plainCollection(data));
49128
49349
  }
49129
49350
  );
49351
+ server.registerTool(
49352
+ "outlook_get_attachment",
49353
+ {
49354
+ description: "Get one attachment's content (ids from outlook_list_attachments). An image comes back as image content the model can see; a text-like file (text/*, JSON, CSV, XML, .ics) is decoded to `text`, capped at 200 KB (`textTruncated` marks a cut). Any other binary (PDF, Office, archives) and anything over 5 MB is returned as metadata only \u2014 its bytes are not inlined. An attached Outlook item (a forwarded email or event) or a cloud-file link has no bytes to return; the result says which. " + UNTRUSTED_DESCRIPTION_SUFFIX,
49355
+ annotations: { readOnlyHint: true, openWorldHint: true },
49356
+ inputSchema: external_exports.object({
49357
+ messageId: external_exports.string().min(1).describe("Message Id"),
49358
+ attachmentId: external_exports.string().min(1).describe("Attachment Id from outlook_list_attachments")
49359
+ })
49360
+ },
49361
+ async ({ messageId, attachmentId }) => {
49362
+ const path = `/me/messages/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachmentId)}`;
49363
+ const meta3 = await client2.get(path, { query: { $select: ATTACHMENT_META_SELECT } });
49364
+ const kind = attachmentKind(meta3);
49365
+ const contentType = meta3.ContentType ?? "";
49366
+ const name = meta3.Name ?? "";
49367
+ const out = stripUndefined({
49368
+ messageId,
49369
+ attachmentId,
49370
+ name: meta3.Name,
49371
+ contentType: meta3.ContentType ?? void 0,
49372
+ size: meta3.Size,
49373
+ isInline: meta3.IsInline || void 0,
49374
+ kind,
49375
+ inlined: false
49376
+ });
49377
+ if (kind === "item") {
49378
+ out.hint = "This is an attached Outlook item (an email, event or contact), not a file, so it has no bytes to return. Ask the sender for it, or open the parent message in Outlook.";
49379
+ return mailboxUntrusted(out);
49380
+ }
49381
+ if (kind === "reference") {
49382
+ out.hint = "This is a link to a cloud file (OneDrive/SharePoint), not a file stored in the message, so there are no bytes to return.";
49383
+ return mailboxUntrusted(out);
49384
+ }
49385
+ const image = isImage(contentType);
49386
+ if (!image && !isText(contentType, name)) {
49387
+ out.hint = "Binary content is not inlined: only images and text-like files are returned. Open it in Outlook to read it.";
49388
+ return mailboxUntrusted(out);
49389
+ }
49390
+ if ((meta3.Size ?? 0) > MAX_INLINE_BYTES) {
49391
+ out.hint = "Content is not inlined above 5 MB. Open it in Outlook to read it.";
49392
+ return mailboxUntrusted(out);
49393
+ }
49394
+ const full = await client2.get(path);
49395
+ const bytes = full.ContentBytes;
49396
+ if (typeof bytes !== "string" || bytes.length === 0) {
49397
+ out.hint = "Outlook returned no content for this attachment.";
49398
+ return mailboxUntrusted(out);
49399
+ }
49400
+ if (base64DecodedLength(bytes) > MAX_INLINE_BYTES) {
49401
+ out.hint = "Content is not inlined above 5 MB. Open it in Outlook to read it.";
49402
+ return mailboxUntrusted(out);
49403
+ }
49404
+ const buf = Buffer.from(bytes, "base64");
49405
+ out.inlined = true;
49406
+ if (image) {
49407
+ const env = mailboxUntrusted(out);
49408
+ return { ...env, content: [...env.content, ...imageResult(bytes, contentType).content] };
49409
+ }
49410
+ out.text = buf.subarray(0, utf8Boundary(buf, MAX_TEXT_BYTES)).toString("utf8");
49411
+ if (buf.length > MAX_TEXT_BYTES) out.textTruncated = true;
49412
+ return mailboxUntrusted(out);
49413
+ }
49414
+ );
49130
49415
  }
49131
49416
 
49132
49417
  // src/tools/calendar.ts
@@ -49153,7 +49438,7 @@ async function mailboxTimeZone(client2, opts = {}) {
49153
49438
  }
49154
49439
 
49155
49440
  // src/tools/calendar.ts
49156
- var EVENT_SELECT = "Id,Subject,Start,End,Location,Organizer,IsAllDay,IsCancelled,ShowAs,OnlineMeetingUrl,OnlineMeeting,BodyPreview";
49441
+ var EVENT_SELECT = "Id,Subject,Start,End,Location,Organizer,IsAllDay,IsCancelled,ShowAs,OnlineMeetingUrl,OnlineMeeting,BodyPreview,IsOrganizer,ResponseStatus,ResponseRequested,Type,SeriesMasterId";
49157
49442
  function dateTimeTimeZone(value, zone) {
49158
49443
  if (/(Z|[+-]\d{2}:?\d{2})$/i.test(value)) {
49159
49444
  const d = new Date(value);
@@ -49258,12 +49543,13 @@ function registerCalendarTools(server, client2) {
49258
49543
  server.registerTool(
49259
49544
  "outlook_find_meeting_times",
49260
49545
  {
49261
- description: "Find times when people can meet \u2014 Outlook's Scheduling Assistant. Give the attendees, a search window and a duration; Outlook checks everyone's free/busy (and the signed-in user's, as organizer) and returns ranked candidate slots with each attendee's availability. By default only working hours are searched. Use this to answer \"when can X and Y meet?\"; then book with outlook_create_event. `start`/`end` are local wall-clock times in `timeZone` (default: the mailbox zone), and returned slots are in that zone too. " + UNTRUSTED_DESCRIPTION_SUFFIX,
49546
+ description: "Find times when people can meet \u2014 Outlook's Scheduling Assistant. Give the attendees, a search window and a duration; Outlook checks everyone's free/busy (and the signed-in user's, as organizer) and returns ranked candidate slots with each attendee's availability. By default only working hours are searched. Use this to answer \"when can X and Y meet?\"; then book with outlook_create_event \u2014 each suggestion carries `createEventArgs` (times, zone and attendees; `subject` too when given here) to pass to it as-is. `start`/`end` are local wall-clock times in `timeZone` (default: the mailbox zone), and returned slots are in that zone too. view: 'raw' returns Outlook's response as-is, without `createEventArgs`. " + UNTRUSTED_DESCRIPTION_SUFFIX,
49262
49547
  annotations: { readOnlyHint: true, openWorldHint: true },
49263
49548
  inputSchema: external_exports.object({
49264
49549
  view: viewParam(VIEWS2),
49265
49550
  attendees: attendeeList("required attendees").min(1),
49266
49551
  optionalAttendees: attendeeList("optional attendees").optional(),
49552
+ subject: external_exports.string().optional().describe("Meeting title, carried into each suggestion's createEventArgs; not sent to Outlook"),
49267
49553
  start: external_exports.string().min(1).describe("Earliest the meeting may start, e.g. 2026-10-12T09:00:00"),
49268
49554
  end: external_exports.string().min(1).describe("Latest the meeting may end, e.g. 2026-10-16T17:00:00"),
49269
49555
  durationMinutes: external_exports.number().int().min(5).max(1440).describe("Meeting length in minutes"),
@@ -49299,19 +49585,37 @@ function registerCalendarTools(server, client2) {
49299
49585
  };
49300
49586
  const data = await client2.post("/me/findmeetingtimes", body, { prefer: `outlook.timezone="${zone}"` });
49301
49587
  if (resolveView(args.view, VIEWS2) === "raw") return mailboxUntrusted(data);
49302
- const suggestions = (data?.MeetingTimeSuggestions ?? []).map((s) => ({
49303
- Start: trimTime(s.MeetingTimeSlot?.Start?.DateTime),
49304
- End: trimTime(s.MeetingTimeSlot?.End?.DateTime),
49305
- Confidence: s.Confidence,
49306
- Organizer: s.OrganizerAvailability,
49307
- Attendees: Object.fromEntries(
49308
- (s.AttendeeAvailability ?? []).map((a) => [
49309
- a.Attendee?.EmailAddress?.Address ?? a.Attendee?.EmailAddress?.Name ?? "?",
49310
- a.Availability
49311
- ])
49312
- ),
49313
- Reason: s.SuggestionReason
49314
- }));
49588
+ const suggestions = (data?.MeetingTimeSuggestions ?? []).map((s) => {
49589
+ const start = trimTime(s.MeetingTimeSlot?.Start?.DateTime);
49590
+ const end = trimTime(s.MeetingTimeSlot?.End?.DateTime);
49591
+ return {
49592
+ Start: start,
49593
+ End: end,
49594
+ Confidence: s.Confidence,
49595
+ Organizer: s.OrganizerAvailability,
49596
+ Attendees: Object.fromEntries(
49597
+ (s.AttendeeAvailability ?? []).map((a) => [
49598
+ a.Attendee?.EmailAddress?.Address ?? a.Attendee?.EmailAddress?.Name ?? "?",
49599
+ a.Availability
49600
+ ])
49601
+ ),
49602
+ Reason: s.SuggestionReason,
49603
+ // outlook_create_event's input for this slot, so booking it is a
49604
+ // straight hand-off. The slot came back in `zone` (the Prefer header),
49605
+ // which is also the zone create reads wall-clock times in. A slot
49606
+ // without times cannot be booked, so it gets no args.
49607
+ ...start && end ? {
49608
+ createEventArgs: {
49609
+ ...args.subject !== void 0 ? { subject: args.subject } : {},
49610
+ start,
49611
+ end,
49612
+ timeZone: zone,
49613
+ attendees: args.attendees,
49614
+ ...args.optionalAttendees?.length ? { optionalAttendees: args.optionalAttendees } : {}
49615
+ }
49616
+ } : {}
49617
+ };
49618
+ });
49315
49619
  return mailboxUntrusted({
49316
49620
  timeZone: zone,
49317
49621
  count: suggestions.length,
@@ -49478,6 +49782,21 @@ function registerDirectoryTools(server, client2) {
49478
49782
  return minifiedResult(collection(data, resolveView(view, VIEWS2) === "raw"));
49479
49783
  }
49480
49784
  );
49785
+ server.registerTool(
49786
+ "outlook_list_categories",
49787
+ {
49788
+ description: "List the mailbox's categories (the master list) with their colours \u2014 the names outlook_update_message can add or remove.",
49789
+ annotations: { readOnlyHint: true, openWorldHint: true },
49790
+ inputSchema: external_exports.object({})
49791
+ },
49792
+ async () => {
49793
+ const data = await client2.get(
49794
+ "/me/outlook/masterCategories"
49795
+ );
49796
+ const categories = (data?.value ?? []).map((c) => ({ name: c.DisplayName, color: c.Color }));
49797
+ return minifiedResult({ count: categories.length, categories });
49798
+ }
49799
+ );
49481
49800
  }
49482
49801
 
49483
49802
  // src/tools/writes.ts
@@ -49530,9 +49849,72 @@ async function writeOrUnknown(run, checkWhere) {
49530
49849
  };
49531
49850
  }
49532
49851
  }
49852
+ var INVITE_EVENT_SELECT = "Id,Subject,IsOrganizer,IsCancelled,Start,End,Organizer,ResponseStatus";
49853
+ var DELETE_EVENT_SELECT = "Id,Subject,IsOrganizer,IsCancelled,ResponseStatus,Type,SeriesMasterId,Start,End,Organizer,Attendees";
49854
+ var INVITE_RESPONSES = {
49855
+ accept: { verb: "accept", status: "Accepted" },
49856
+ tentative: { verb: "tentativelyaccept", status: "TentativelyAccepted" },
49857
+ decline: { verb: "decline", status: "Declined" }
49858
+ };
49533
49859
  var naiveMs = (dt) => Date.parse(`${dt.replace(/\.\d+$/, "")}Z`);
49534
49860
  var fromNaiveMs = (ms) => new Date(ms).toISOString().replace(/\.\d{3}Z$/, "");
49535
49861
  var trimTime2 = (dt) => dt?.replace(/\.\d+$/, "");
49862
+ var REPLY_MODES = {
49863
+ reply: { send: "reply", draft: "createreply", prefix: "RE: " },
49864
+ replyAll: { send: "replyall", draft: "createreplyall", prefix: "RE: " },
49865
+ forward: { send: "forward", draft: "createforward", prefix: "FW: " }
49866
+ };
49867
+ function addresses(...lists) {
49868
+ const seen = /* @__PURE__ */ new Map();
49869
+ for (const a of lists.flat()) {
49870
+ const v = a?.EmailAddress?.Address;
49871
+ if (v && !seen.has(v.toLowerCase())) seen.set(v.toLowerCase(), v);
49872
+ }
49873
+ return [...seen.values()];
49874
+ }
49875
+ var escapeHtml = (t) => t.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
49876
+ function bodyWithComment(body, comment) {
49877
+ const existing = body?.Content ?? "";
49878
+ if (body?.ContentType?.toLowerCase() !== "html") {
49879
+ return { ContentType: "Text", Content: existing ? `${comment}
49880
+
49881
+ ${existing}` : comment };
49882
+ }
49883
+ const block = `<div>${escapeHtml(comment).replace(/\r?\n/g, "<br>")}</div><br>`;
49884
+ const open2 = /<body[^>]*>/i.exec(existing);
49885
+ const Content = open2 ? existing.slice(0, open2.index + open2[0].length) + block + existing.slice(open2.index + open2[0].length) : block + existing;
49886
+ return { ContentType: "HTML", Content };
49887
+ }
49888
+ var FLAG_STATUS = { flagged: "Flagged", complete: "Complete", none: "NotFlagged" };
49889
+ var UPDATE_CONCURRENCY = 4;
49890
+ var sameCategories = (a, b) => [...a ?? []].map((c) => c.toLowerCase()).sort().join("\n") === [...b ?? []].map((c) => c.toLowerCase()).sort().join("\n");
49891
+ function editCategories(current, add, remove) {
49892
+ const gone = new Set(remove.map((c) => c.toLowerCase()));
49893
+ const next = (current ?? []).filter((c) => !gone.has(c.toLowerCase()));
49894
+ for (const c of add) {
49895
+ if (!next.some((n) => n.toLowerCase() === c.toLowerCase())) next.push(c);
49896
+ }
49897
+ return next;
49898
+ }
49899
+ function unchangedFields(payload, after) {
49900
+ const out = [];
49901
+ if (payload.IsRead !== void 0 && after.IsRead !== payload.IsRead) out.push("IsRead");
49902
+ if (payload.Flag && after.Flag?.FlagStatus !== payload.Flag.FlagStatus) out.push("Flag");
49903
+ if (payload.Categories && !sameCategories(after.Categories, payload.Categories)) out.push("Categories");
49904
+ return out;
49905
+ }
49906
+ var createEventInput = external_exports.object({
49907
+ subject: external_exports.string().describe("Event title"),
49908
+ start: external_exports.string().min(1).describe("Start, ISO 8601 local time e.g. 2026-09-22T15:00:00"),
49909
+ end: external_exports.string().min(1).describe("End, ISO 8601 local time"),
49910
+ timeZone: external_exports.string().optional().describe("Windows time-zone name. Defaults to the MAILBOX time zone."),
49911
+ location: external_exports.string().optional().describe("Location display name"),
49912
+ body: external_exports.string().optional().describe("Event description"),
49913
+ attendees: recipientList.describe("Required attendees' email addresses"),
49914
+ optionalAttendees: recipientList.describe("Optional attendees' email addresses"),
49915
+ teamsMeeting: teamsMeetingParam,
49916
+ confirmToken: confirmTokenParam
49917
+ });
49536
49918
  function registerWriteTools(server, client2) {
49537
49919
  server.registerTool(
49538
49920
  "outlook_send_mail",
@@ -49651,6 +50033,85 @@ function registerWriteTools(server, client2) {
49651
50033
  });
49652
50034
  }
49653
50035
  );
50036
+ server.registerTool(
50037
+ "outlook_update_message",
50038
+ {
50039
+ description: 'Flag, categorise and/or mark read up to 50 messages at once \u2014 the filing step of the triage loop (outlook_get_unread \u2192 act \u2192 update). `flag`: "flagged", "complete" or "none". `categories` replaces a message\'s categories; `addCategories`/`removeCategories` edit each message\'s current list instead (names from outlook_list_categories). Duplicate ids are collapsed, so each message is written once. Each id gets its own result `{ id, ok, error? }`, verified against what Outlook stored; one failure does not stop the rest, but a rejected or missing credential fails the whole call. Only your mailbox changes \u2014 nobody is notified. ' + CONFIRM_FLOW_SENTENCE,
50040
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
50041
+ inputSchema: external_exports.object({
50042
+ messageIds: external_exports.array(external_exports.string().min(1)).min(1).max(50).describe("Message Ids (1 to 50)"),
50043
+ flag: external_exports.enum(["flagged", "complete", "none"]).optional().describe("Follow-up flag to set"),
50044
+ categories: external_exports.array(external_exports.string().min(1)).optional().describe("Replace the categories with exactly these ([] clears them)"),
50045
+ addCategories: external_exports.array(external_exports.string().min(1)).optional().describe("Categories to add"),
50046
+ removeCategories: external_exports.array(external_exports.string().min(1)).optional().describe("Categories to remove"),
50047
+ isRead: external_exports.boolean().optional().describe("true to mark read, false to mark unread"),
50048
+ confirmToken: confirmTokenParam
50049
+ })
50050
+ },
50051
+ async ({ messageIds, flag, categories, addCategories, removeCategories, isRead, confirmToken }, ctx) => {
50052
+ const add = addCategories ?? [];
50053
+ const remove = removeCategories ?? [];
50054
+ const editing = add.length > 0 || remove.length > 0;
50055
+ if (categories !== void 0 && editing) {
50056
+ throw new McpToolError("Pass `categories` or `addCategories`/`removeCategories`, not both.", {
50057
+ hint: "`categories` replaces the whole list; the add/remove pair edits the current one."
50058
+ });
50059
+ }
50060
+ const fixed = {
50061
+ ...isRead !== void 0 ? { IsRead: isRead } : {},
50062
+ ...flag ? { Flag: { FlagStatus: FLAG_STATUS[flag] } } : {},
50063
+ ...categories !== void 0 ? { Categories: categories } : {}
50064
+ };
50065
+ if (Object.keys(fixed).length === 0 && !editing) {
50066
+ throw new McpToolError("Nothing to update.", {
50067
+ hint: "Pass at least one of flag, isRead, categories, addCategories or removeCategories."
50068
+ });
50069
+ }
50070
+ const ids = [...new Set(messageIds)];
50071
+ const pathOf = (id) => `/me/messages/${encodeURIComponent(id)}`;
50072
+ const changes = [
50073
+ ...flag ? [`flag ${flag}`] : [],
50074
+ ...isRead !== void 0 ? [isRead ? "mark read" : "mark unread"] : [],
50075
+ ...categories !== void 0 ? [`set categories [${categories.join(", ")}]`] : [],
50076
+ ...add.length ? [`add categories [${add.join(", ")}]`] : [],
50077
+ ...remove.length ? [`remove categories [${remove.join(", ")}]`] : []
50078
+ ];
50079
+ const gate = await confirmWrite(ctx, {
50080
+ tool: "outlook_update_message",
50081
+ action: "mail.update",
50082
+ message: "Review and confirm this change:",
50083
+ summary: `Update ${ids.length} message${ids.length === 1 ? "" : "s"}: ${changes.join("; ")}`,
50084
+ // One signed-in mailbox per server process.
50085
+ account: void 0,
50086
+ target: ids.join(","),
50087
+ request: {
50088
+ method: "PATCH",
50089
+ path: ids.length === 1 ? pathOf(ids[0]) : "/me/messages/{id}",
50090
+ body: editing ? { ...fixed, Categories: `(each message's current categories${add.length ? ` + ${add.join(", ")}` : ""}${remove.length ? ` - ${remove.join(", ")}` : ""})` } : fixed
50091
+ },
50092
+ confirmToken
50093
+ });
50094
+ if (gate) return gate;
50095
+ const results = await mapWithConcurrency(ids, UPDATE_CONCURRENCY, async (id) => {
50096
+ try {
50097
+ const payload = { ...fixed };
50098
+ if (editing) {
50099
+ const current = await client2.get(`${pathOf(id)}?$select=Categories`);
50100
+ payload.Categories = editCategories(current?.Categories, add, remove);
50101
+ }
50102
+ const echoed = await client2.write("PATCH", pathOf(id), payload);
50103
+ const after = echoed && typeof echoed === "object" ? echoed : await client2.get(`${pathOf(id)}?$select=IsRead,Flag,Categories`) ?? {};
50104
+ const unchanged = unchangedFields(payload, after);
50105
+ return unchanged.length ? { id, ok: false, error: `Outlook accepted the write but these did not change: ${unchanged.join(", ")}.` } : { id, ok: true };
50106
+ } catch (e) {
50107
+ if (isCredentialFailure(e)) throw e;
50108
+ return { id, ok: false, error: e instanceof Error ? e.message : String(e) };
50109
+ }
50110
+ });
50111
+ const updated = results.filter((r) => r.ok).length;
50112
+ return minifiedResult({ updated, failed: results.length - updated, results });
50113
+ }
50114
+ );
49654
50115
  server.registerTool(
49655
50116
  "outlook_move_message",
49656
50117
  {
@@ -49693,20 +50154,9 @@ function registerWriteTools(server, client2) {
49693
50154
  server.registerTool(
49694
50155
  "outlook_create_event",
49695
50156
  {
49696
- description: 'Create a calendar event or meeting. A Microsoft Teams meeting is attached by default (`teamsMeeting: false` to skip) and its join link is returned. `timeZone` takes a WINDOWS zone name such as "Eastern Standard Time", not an IANA name. Attendees are emailed an invitation. To pick a time first, use outlook_find_meeting_times. ' + CONFIRM_FLOW_SENTENCE + " " + TIMEOUT_SENTENCE + " " + CONFIRM_INJECTION_RULE,
50157
+ description: 'Create a calendar event or meeting. A Microsoft Teams meeting is attached by default (`teamsMeeting: false` to skip) and its join link is returned. `timeZone` takes a WINDOWS zone name such as "Eastern Standard Time", not an IANA name. Attendees are emailed an invitation. To pick a time first, use outlook_find_meeting_times: each suggestion it returns carries `createEventArgs` that can be passed here as-is (add `subject` if the search did not set one). ' + CONFIRM_FLOW_SENTENCE + " " + TIMEOUT_SENTENCE + " " + CONFIRM_INJECTION_RULE,
49697
50158
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
49698
- inputSchema: external_exports.object({
49699
- subject: external_exports.string().describe("Event title"),
49700
- start: external_exports.string().min(1).describe("Start, ISO 8601 local time e.g. 2026-09-22T15:00:00"),
49701
- end: external_exports.string().min(1).describe("End, ISO 8601 local time"),
49702
- timeZone: external_exports.string().optional().describe("Windows time-zone name. Defaults to the MAILBOX time zone."),
49703
- location: external_exports.string().optional().describe("Location display name"),
49704
- body: external_exports.string().optional().describe("Event description"),
49705
- attendees: recipientList.describe("Required attendees' email addresses"),
49706
- optionalAttendees: recipientList.describe("Optional attendees' email addresses"),
49707
- teamsMeeting: teamsMeetingParam,
49708
- confirmToken: confirmTokenParam
49709
- })
50159
+ inputSchema: createEventInput
49710
50160
  },
49711
50161
  async ({ subject, start, end, timeZone, location, body, attendees, optionalAttendees, teamsMeeting, confirmToken }, ctx) => {
49712
50162
  const wantTeams = teamsMeeting !== false;
@@ -49874,6 +50324,282 @@ function registerWriteTools(server, client2) {
49874
50324
  });
49875
50325
  }
49876
50326
  );
50327
+ server.registerTool(
50328
+ "outlook_respond_to_invite",
50329
+ {
50330
+ description: "Accept, tentatively accept or decline a meeting invitation. Pass the invite's `messageId` (from outlook_get_unread, which marks it kind \"meetingRequest\") or the event's `eventId` \u2014 exactly one. The organizer is sent your response unless `sendResponse: false`, in which case only your calendar changes and no confirmation is asked. Cancelled meetings and meetings you organize are refused. The result is verified by re-reading the event's response status; a declined meeting may leave your calendar entirely. " + CONFIRM_FLOW_SENTENCE + " " + TIMEOUT_SENTENCE + " " + CONFIRM_INJECTION_RULE,
50331
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
50332
+ inputSchema: external_exports.object({
50333
+ messageId: external_exports.string().min(1).optional().describe("Id of the invite message (resolved to its event). Give this or eventId."),
50334
+ eventId: external_exports.string().min(1).optional().describe("Event Id. Give this or messageId."),
50335
+ response: external_exports.enum(["accept", "tentative", "decline"]).describe("Your answer"),
50336
+ comment: external_exports.string().optional().describe("Note to the organizer sent with the response"),
50337
+ sendResponse: external_exports.boolean().optional().describe("Tell the organizer (default true). false changes only your calendar."),
50338
+ confirmToken: confirmTokenParam
50339
+ })
50340
+ },
50341
+ async ({ messageId, eventId, response, comment, sendResponse, confirmToken }, ctx) => {
50342
+ if (messageId === void 0 === (eventId === void 0)) {
50343
+ throw new McpToolError("Pass exactly one of messageId or eventId.", {
50344
+ hint: "messageId is the invite in your inbox; eventId is the meeting on your calendar."
50345
+ });
50346
+ }
50347
+ let event;
50348
+ if (messageId !== void 0) {
50349
+ const msg = await client2.get(
50350
+ `/me/messages/${encodeURIComponent(messageId)}`,
50351
+ { query: { $select: `Id,${MEETING_MESSAGE_TYPE}`, $expand: EVENT_EXPAND } }
50352
+ );
50353
+ event = resolvedEvent(msg?.Event);
50354
+ if (!event) {
50355
+ if (isMeetingMessage(msg)) {
50356
+ throw new McpToolError(
50357
+ "This invite's meeting is no longer on your calendar (already declined, cancelled or deleted) \u2014 there is nothing to respond to.",
50358
+ { hint: "Mark the invite read or file it; answering it again is not possible." }
50359
+ );
50360
+ }
50361
+ throw new McpToolError("That message is not a meeting invite.", {
50362
+ hint: 'Only messages outlook_get_unread marks kind "meetingRequest" carry an event to answer.'
50363
+ });
50364
+ }
50365
+ } else {
50366
+ event = await client2.get(
50367
+ `/me/events/${encodeURIComponent(eventId)}?$select=${INVITE_EVENT_SELECT}`
50368
+ );
50369
+ }
50370
+ const id = event?.Id ?? eventId;
50371
+ if (event?.IsCancelled === true) {
50372
+ throw new McpToolError("This meeting has been cancelled; there is nothing to respond to.", {
50373
+ hint: "Remove it from your calendar with outlook_delete_event if it is still shown."
50374
+ });
50375
+ }
50376
+ if (event?.IsOrganizer === true || event?.ResponseStatus?.Response === "Organizer") {
50377
+ throw new McpToolError("You organize this meeting, so you cannot respond to it.", {
50378
+ hint: "To change or call it off, use outlook_update_event or Outlook itself."
50379
+ });
50380
+ }
50381
+ const { verb, status } = INVITE_RESPONSES[response];
50382
+ const notify = sendResponse !== false;
50383
+ if (!notify && comment) {
50384
+ throw new McpToolError("A comment can only go with a response the organizer receives.", {
50385
+ hint: "Drop the comment, or leave sendResponse at its default (true) so the organizer gets both."
50386
+ });
50387
+ }
50388
+ const path = `/me/events/${encodeURIComponent(id)}/${verb}`;
50389
+ const payload = { ...comment ? { Comment: comment } : {}, SendResponse: notify };
50390
+ if (notify) {
50391
+ const when = `${trimTime2(event?.Start?.DateTime) ?? "?"} to ${trimTime2(event?.End?.DateTime) ?? "?"}${event?.Start?.TimeZone ? ` (${event.Start.TimeZone})` : ""}`;
50392
+ const organizer = event?.Organizer?.EmailAddress?.Address ?? "the organizer";
50393
+ const gate = await confirmWrite(ctx, {
50394
+ tool: "outlook_respond_to_invite",
50395
+ action: "calendar.respond_to_invite",
50396
+ message: "Review and confirm this response (the organizer is notified):",
50397
+ summary: `${response === "tentative" ? "Tentatively accept" : response === "accept" ? "Accept" : "Decline"} "${event?.Subject ?? id}" ${when}, organized by ${organizer}` + (comment ? `, with comment: "${comment}"` : ""),
50398
+ // One signed-in mailbox per server process.
50399
+ account: void 0,
50400
+ target: id,
50401
+ request: { method: "POST", path, body: payload },
50402
+ confirmToken
50403
+ });
50404
+ if (gate) return gate;
50405
+ }
50406
+ const write = await writeOrUnknown(
50407
+ () => client2.write("POST", path, payload),
50408
+ "the event's response (outlook_get_event)"
50409
+ );
50410
+ if (!write.ok) return write.result;
50411
+ let after;
50412
+ try {
50413
+ after = await client2.get(
50414
+ `/me/events/${encodeURIComponent(id)}?$select=ResponseStatus`
50415
+ );
50416
+ } catch (e) {
50417
+ if (response === "decline" && errorStatusOf(e) === 404) {
50418
+ return minifiedResult({
50419
+ responded: true,
50420
+ eventId: id,
50421
+ response: status,
50422
+ sentResponse: notify,
50423
+ note: "The declined meeting is no longer on your calendar."
50424
+ });
50425
+ }
50426
+ return minifiedResult({
50427
+ responded: null,
50428
+ eventId: id,
50429
+ sentResponse: notify,
50430
+ warning: `Outlook accepted the response but re-reading the event failed (${e instanceof Error ? e.message : String(e)}), so it is unverified.`
50431
+ });
50432
+ }
50433
+ const got = after?.ResponseStatus?.Response;
50434
+ return minifiedResult({
50435
+ responded: got === status,
50436
+ eventId: id,
50437
+ response: got,
50438
+ sentResponse: notify,
50439
+ ...got === status ? {} : { warning: `Outlook accepted the response but the status did not change (still ${got ?? "unknown"}).` }
50440
+ });
50441
+ }
50442
+ );
50443
+ server.registerTool(
50444
+ "outlook_delete_event",
50445
+ {
50446
+ description: `Delete a calendar event. An occurrence's id deletes only that occurrence; a series master's id (Type "SeriesMaster" in outlook_get_event) deletes the WHOLE series. Only the organizer can delete a meeting \u2014 for an invite you received, decline it with outlook_respond_to_invite instead (a meeting the organizer already cancelled can be removed here). Deleting a meeting that has attendees sends each of them a cancellation, so it is confirmed first; your own appointment with no attendees, or a cancelled meeting, is removed without asking because only your calendar changes. The result is verified by re-reading the event. ` + CONFIRM_FLOW_SENTENCE + " " + TIMEOUT_SENTENCE + " " + CONFIRM_INJECTION_RULE,
50447
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
50448
+ inputSchema: external_exports.object({
50449
+ id: external_exports.string().min(1).describe("Event Id from outlook_list_events or outlook_get_event (an occurrence, or a series master)"),
50450
+ confirmToken: confirmTokenParam
50451
+ })
50452
+ },
50453
+ async ({ id, confirmToken }, ctx) => {
50454
+ const path = `/me/events/${encodeURIComponent(id)}`;
50455
+ const zone = await mailboxTimeZone(client2);
50456
+ const event = await client2.get(
50457
+ `${path}?$select=${DELETE_EVENT_SELECT}`,
50458
+ zone ? { prefer: `outlook.timezone="${zone}"` } : {}
50459
+ );
50460
+ const organizer = event?.IsOrganizer === true || event?.ResponseStatus?.Response === "Organizer";
50461
+ const cancelled = event?.IsCancelled === true;
50462
+ if (!organizer && !cancelled) {
50463
+ throw new McpToolError("Only the organizer can delete this meeting.", {
50464
+ hint: 'You are an attendee: deleting your copy would not tell the organizer. To drop it, decline with outlook_respond_to_invite (response: "decline"), which also removes it from your calendar.'
50465
+ });
50466
+ }
50467
+ const scope = event?.Type === "SeriesMaster" ? "the whole series (every occurrence)" : event?.Type === "Occurrence" || event?.Type === "Exception" ? "only this occurrence (the rest of the series stays)" : "this event";
50468
+ const attendeeCount = event?.Attendees?.length ?? 0;
50469
+ const notifies = organizer && !cancelled && attendeeCount > 0;
50470
+ if (notifies) {
50471
+ const when = `${trimTime2(event?.Start?.DateTime) ?? "?"} to ${trimTime2(event?.End?.DateTime) ?? "?"}${event?.Start?.TimeZone ? ` (${event.Start.TimeZone})` : ""}`;
50472
+ const gate = await confirmWrite(ctx, {
50473
+ tool: "outlook_delete_event",
50474
+ action: "calendar.delete_event",
50475
+ message: "Review and confirm this deletion (every attendee is sent a cancellation):",
50476
+ summary: `Delete "${event?.Subject ?? id}" ${when} \u2014 ${scope}; ${attendeeCount} attendee${attendeeCount === 1 ? "" : "s"} will be sent a cancellation`,
50477
+ // One signed-in mailbox per server process.
50478
+ account: void 0,
50479
+ target: id,
50480
+ request: { method: "DELETE", path },
50481
+ preview: { scope, attendees: attendeeCount },
50482
+ confirmToken
50483
+ });
50484
+ if (gate) return gate;
50485
+ }
50486
+ const write = await writeOrUnknown(
50487
+ () => client2.write("DELETE", path),
50488
+ "the calendar (outlook_get_event)"
50489
+ );
50490
+ if (!write.ok) return write.result;
50491
+ const base = { Id: id, Subject: event?.Subject, scope, cancellationSent: notifies };
50492
+ let after;
50493
+ try {
50494
+ after = await client2.get(`${path}?$select=Id`);
50495
+ } catch (e) {
50496
+ if (errorStatusOf(e) === 404) return minifiedResult({ deleted: true, ...base });
50497
+ return minifiedResult({
50498
+ deleted: null,
50499
+ ...base,
50500
+ warning: `Outlook accepted the delete but re-reading the event failed (${e instanceof Error ? e.message : String(e)}), so it is unverified.`
50501
+ });
50502
+ }
50503
+ return minifiedResult({
50504
+ deleted: false,
50505
+ ...base,
50506
+ warning: `Outlook accepted the delete but the event is still on the calendar${after?.Id ? ` (${after.Id})` : ""}.`
50507
+ });
50508
+ }
50509
+ );
50510
+ server.registerTool(
50511
+ "outlook_reply",
50512
+ {
50513
+ description: "Reply, reply-all or forward a message, with `comment` as your new text above the quoted original. Outlook picks the recipients for reply (the sender, or its Reply-To) and reply-all (sender plus the original To and Cc, minus you); a forward needs `to`. `draftOnly: true` leaves the reply in Drafts and returns its id, sending nothing and asking no confirmation; otherwise it is sent at once. " + CONFIRM_FLOW_SENTENCE + " The preview shows the recipients, subject and comment. " + TIMEOUT_SENTENCE + " " + CONFIRM_INJECTION_RULE + " " + UNTRUSTED_DESCRIPTION_SUFFIX,
50514
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
50515
+ inputSchema: external_exports.object({
50516
+ messageId: external_exports.string().min(1).describe("Id of the message to answer or forward"),
50517
+ mode: external_exports.enum(["reply", "replyAll", "forward"]).describe("reply (sender only), replyAll, or forward"),
50518
+ comment: external_exports.string().describe("Your text, placed above the quoted original"),
50519
+ to: recipientList.describe("Forward recipients' email addresses (forward only, required there)"),
50520
+ draftOnly: external_exports.boolean().optional().describe("Save to Drafts instead of sending (default false)"),
50521
+ confirmToken: confirmTokenParam
50522
+ })
50523
+ },
50524
+ async ({ messageId, mode, comment, to, draftOnly, confirmToken }, ctx) => {
50525
+ if (mode === "forward" && !to?.length) {
50526
+ throw new McpToolError("A forward needs at least one recipient.", {
50527
+ hint: "Pass `to` with the addresses to forward to."
50528
+ });
50529
+ }
50530
+ if (mode !== "forward" && to?.length) {
50531
+ throw new McpToolError("`to` applies only to a forward.", {
50532
+ hint: 'A reply goes where Outlook computes; use mode "forward" to send it somewhere else, or outlook_send_mail.'
50533
+ });
50534
+ }
50535
+ const { send, draft, prefix } = REPLY_MODES[mode];
50536
+ const base = `/me/messages/${encodeURIComponent(messageId)}`;
50537
+ const forwardTo = mode === "forward" ? { ToRecipients: toRecipients(to) } : {};
50538
+ if (draftOnly === true) {
50539
+ const create = await writeOrUnknown(
50540
+ () => client2.write(
50541
+ "POST",
50542
+ `${base}/${draft}`,
50543
+ {}
50544
+ ),
50545
+ "Drafts"
50546
+ );
50547
+ if (!create.ok) return create.result;
50548
+ const created = create.value;
50549
+ if (!created?.Id) {
50550
+ throw new McpToolError("Outlook did not return the draft it created.", {
50551
+ hint: "Check the Drafts folder before trying again."
50552
+ });
50553
+ }
50554
+ try {
50555
+ await client2.write("PATCH", `/me/messages/${encodeURIComponent(created.Id)}`, {
50556
+ Body: bodyWithComment(created.Body, comment),
50557
+ ...forwardTo
50558
+ });
50559
+ } catch (e) {
50560
+ return minifiedResult({
50561
+ drafted: true,
50562
+ mode,
50563
+ draftId: created.Id,
50564
+ WebLink: created.WebLink,
50565
+ warning: `The draft was created but adding your comment${mode === "forward" ? " and recipients" : ""} failed (${e instanceof Error ? e.message : String(e)}). Edit it in Drafts, or delete it before trying again.`
50566
+ });
50567
+ }
50568
+ return minifiedResult({ drafted: true, mode, draftId: created.Id, WebLink: created.WebLink });
50569
+ }
50570
+ const orig = await client2.get(base, {
50571
+ query: { $select: "Subject,From,ReplyTo,ToRecipients,CcRecipients" }
50572
+ });
50573
+ const sender = orig?.ReplyTo?.length ? orig.ReplyTo : orig?.From ? [orig.From] : [];
50574
+ const recipients = mode === "forward" ? { to: to ?? [], cc: [] } : mode === "replyAll" ? { to: addresses(sender, orig?.ToRecipients), cc: addresses(orig?.CcRecipients) } : { to: addresses(sender), cc: [] };
50575
+ const subject = `${prefix}${orig?.Subject ?? ""}`;
50576
+ const path = `${base}/${send}`;
50577
+ const payload = { Comment: comment, ...forwardTo };
50578
+ const everyone = [...recipients.to, ...recipients.cc.map((c) => `cc ${c}`)];
50579
+ const who = everyone.length > MAX_RECIPIENTS ? `${everyone.slice(0, MAX_RECIPIENTS).join(", ")} and ${(everyone.length - MAX_RECIPIENTS).toLocaleString("en-US")} more` : everyone.join(", ");
50580
+ const gate = await confirmWrite(ctx, {
50581
+ tool: "outlook_reply",
50582
+ action: `mail.${send}`,
50583
+ message: "Review and confirm this email before it is sent:",
50584
+ summary: `${mode === "forward" ? "Forward" : mode === "replyAll" ? "Reply all" : "Reply"} "${subject}" to ${who || "(no recipients)"}${mode === "replyAll" ? " (Outlook leaves out your own address)" : ""}, with comment: "${comment}"`,
50585
+ // One signed-in mailbox per server process.
50586
+ account: void 0,
50587
+ target: messageId,
50588
+ request: { method: "POST", path, body: payload },
50589
+ confirmToken
50590
+ });
50591
+ if (gate) return gate;
50592
+ const sent = await writeOrUnknown(() => client2.write("POST", path, payload), "Sent Items");
50593
+ if (!sent.ok) return sent.result;
50594
+ return mailboxUntrusted({
50595
+ sent: true,
50596
+ mode,
50597
+ subject,
50598
+ ...cappedRecipients("to", recipients.to),
50599
+ ...cappedRecipients("cc", recipients.cc)
50600
+ });
50601
+ }
50602
+ );
49877
50603
  }
49878
50604
 
49879
50605
  // src/tools/healthcheck.ts